Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding flow #28

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
110 changes: 110 additions & 0 deletions clang_bind/bind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from pathlib import Path

from clang_bind.cmake_frontend import CMakeFileAPI, CompilationDatabase
from clang_bind.parse import Parse


class Bind:
"""Class to bind C++ targets.

:param source_dir: Source dir of cpp library.
:type source_dir: str
:param build_dir: CMake build dir of cpp library, containing .cmake dir or compile_commands.json
:type build_dir: str
:param output_dir: Output dir
:type output_dir: str
:param output_module_name: Module name in python
:type output_module_name: str
:param cpp_targets: List of C++ targets to bind, defaults to []: bind all.
:type cpp_targets: list, optional
:param allow_inclusions_from_other_targets: Allow inclusions from other targets, which are not specified in cpp_targets, defaults to True
:type allow_inclusions_from_other_targets: bool, optional
"""

def __init__(
self,
source_dir,
build_dir,
output_dir,
output_module_name,
cpp_targets=[],
allow_inclusions_from_other_targets=True,
):
all_cpp_targets = CMakeFileAPI(build_dir).get_library_targets()
if not cpp_targets:
cpp_targets = all_cpp_targets # bind all C++ targets

all_inclusion_sources = []
for target in all_cpp_targets: # for all C++ targets, populate the variable
sources = CMakeFileAPI(build_dir).get_sources(target) # target's sources
cpp_sources = list(
filter(lambda source: source.endswith(".cpp"), sources)
) # sources ending with .cpp
all_inclusion_sources += list(
set(sources) - set(cpp_sources)
) # other sources like .h and .hpp files

self.binding_db = {} # binding database
for target in cpp_targets:
sources = CMakeFileAPI(build_dir).get_sources(target) # target's sources
cpp_sources = list(
filter(lambda source: source.endswith(".cpp"), sources)
) # sources ending with .cpp
inclusion_sources = (
all_inclusion_sources
if allow_inclusions_from_other_targets
else list(set(sources) - set(cpp_sources))
) # other sources like .h and .hpp files

self.binding_db[target] = {
"source_dir": source_dir, # source dir containing C++ targets
"output_dir": output_dir, # output dir
"inclusion_sources": inclusion_sources, # inclusions for the target
"files": [ # list of files' information
{
"source": cpp_source,
"compiler_arguments": CompilationDatabase(
build_dir
).get_compilation_arguments(cpp_source),
}
for cpp_source in cpp_sources
],
}

def _parse(self):
"""For all input files, get the parsed tree and update the db."""
for target in self.binding_db.values():
source_dir = target.get("source_dir")
inclusion_sources = [
str(Path(source_dir, inclusion_source))
for inclusion_source in target.get("inclusion_sources")
] # string full paths of inclusion sources

for file in target.get("files"):
parsed_tree = Parse(
Path(source_dir, file.get("source")),
inclusion_sources,
file.get("compiler_arguments"),
).get_tree()

file.update({"parsed_tree": parsed_tree}) # update db

# Debugging:
#
# - To print the trees:
# parsed_tree.show()
#
# - To save the JSONs:
# import json
# json_output_path = Path(
# target.get("output_dir"),
# "parsed",
# file.get("source").replace(".cpp", ".json"),
# )
# json_output_path.parent.mkdir(parents=True, exist_ok=True)
# with open(json_output_path, 'w') as f:
# json.dump(parsed_tree.to_dict(), f, indent=4)

def bind(self):
"""Function to bind the input files."""
self._parse()
93 changes: 51 additions & 42 deletions clang_bind/cmake_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,38 @@


class CompilationDatabase:
"""Class to get information from a CMake compilation database."""
"""Class to get information from a CMake compilation database.

:param build_dir: Build directory path, where compile_commands.json is present.
:type build_dir: str
"""

def __init__(self, build_dir):
self.compilation_database = clang.CompilationDatabase.fromDirectory(
buildDir=build_dir
)

def get_compilation_arguments(self, filename=None):
"""Returns the compilation commands extracted from the compilation database
def get_compilation_arguments(self, filename):
"""Returns the compilation commands extracted from the compilation database.

:param filename: Get compilation arguments of the file, defaults to None: get for all files
:type filename: str, optional
:return: ilenames and their compiler arguments: {filename: compiler arguments}
:rtype: dict
:param filename: Get compilation arguments of the file.
:type filename: str
:return: Compiler arguments.
:rtype: list
"""

if filename:
# Get compilation commands from the compilation database for the given file
compilation_commands = self.compilation_database.getCompileCommands(
filename=filename
)
else:
# Get all compilation commands from the compilation database
compilation_commands = self.compilation_database.getAllCompileCommands()

return {
command.filename: list(command.arguments)[1:-1]
for command in compilation_commands
}
compilation_arguments = []
for command in self.compilation_database.getCompileCommands(filename=filename):
compilation_arguments += list(command.arguments)[1:-1]
return compilation_arguments


class Target:
"""Class to get information about targets found from the CMake file API."""
"""Class to get information about targets found from the CMake file API.

:param target_file: Target file path.
:type target_file: str
"""

def __init__(self, target_file):
with open(target_file) as f:
Expand Down Expand Up @@ -196,7 +195,11 @@ def get_type(self):


class CMakeFileAPI:
"""CMake File API front end."""
"""CMake File API front end.

:param build_dir: Build directory path, where .cmake directory is present.
:type build_dir: str
"""

def __init__(self, build_dir):
self.reply_dir = Path(build_dir, ".cmake", "api", "v1", "reply")
Expand All @@ -220,29 +223,35 @@ def _set_targets_from_codemodel(self):
target_obj = Target(Path(self.reply_dir, target_file))
self.targets[target_obj.get_name()] = target_obj

def get_dependencies(self, target=None):
"""Get dependencies of the target(s).
def get_library_targets(self):
"""Get all library targets' names.

:param target: Target to get the dependencies, defaults to None
:type target: str, optional
:return: Dependencies of the target(s).
:rtype: dict
:return: Library targets.
:rtype: list
"""
targets = [self.targets.get(target)] if target else self.targets.values()
return {
target.get_name(): list(
map(lambda x: x.split("::")[0], target.get_dependencies())
)
for target in targets
}
library_targets_objs = filter(
lambda target: target.get_type() == "SHARED_LIBRARY", self.targets.values()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about STATIC build?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't see it in pcl, so didn't get to test how it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PCL_SHARED_LIBS can be used to choose between shared and static library for pcl

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please provide a reference link or example?

Copy link
Member

@kunaltyagi kunaltyagi Aug 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmake -DPCL_SHARED_LIBS:BOOL=FALSE or similar

)
return list(map(lambda target: target.get_name(), library_targets_objs))

def get_dependencies(self, target):
"""Get dependencies of the target.

:param target: Target to get the dependencies of.
:type target: str
:return: Dependencies of the target.
:rtype: list
"""
return list(
map(lambda x: x.split("::")[0], self.targets.get(target).get_dependencies())
)

def get_sources(self, target=None):
def get_sources(self, target):
"""Get sources of the target(s).

:param target: Target to get the dependencies, defaults to None
:type target: str, optional
:return: Sources of the target(s).
:rtype: dict
:param target: Target to get the sources of.
:type target: str
:return: Sources of the target.
:rtype: list
"""
targets = [self.targets.get(target)] if target else self.targets.values()
return {target.get_name(): target.get_sources() for target in targets}
return self.targets.get(target).get_sources()
23 changes: 14 additions & 9 deletions clang_bind/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class Parse:
:type compiler_arguments: list, optional
"""

def __init__(self, file, compiler_arguments=[]):
def __init__(self, file, inclusion_sources=[], compiler_arguments=[]):
self.inclusion_sources = inclusion_sources
self._parsed_info_map = {}
index = clang.Index.create()
"""
Expand All @@ -57,27 +58,31 @@ def __init__(self, file, compiler_arguments=[]):
self._construct_tree(self.root_node)

@staticmethod
def is_cursor_in_file(cursor, filename):
"""Checks if the cursor belongs in the file.
def is_cursor_in_files(cursor, files):
"""Checks if the cursor belongs in the files.

:param cursor: An object of :class:`clang.cindex.Cursor`
:type cursor: class:`clang.cindex.Cursor`
:param filename: Filename to search the cursor
:type filename: str
:return: `True` if cursor in file, else `False`
:param files: Filepaths to search the cursor
:type files: list
:return: `True` if cursor in files, else `False`
:rtype: bool
"""
return cursor.location.file and cursor.location.file.name == filename
return cursor.location.file and cursor.location.file.name in files

def _is_valid_child(self, child_cursor):
"""Checks if the child is valid (child should be in the same file as the parent).
"""Checks if the child is valid:
- Either child should be in the same file as the parent or,
- the child should be in the list of valid inclusion sources

:param child_cursor: The child cursor to check, an object of :class:`clang.cindex.Cursor`
:type child_cursor: class:`clang.cindex.Cursor`
:return: `True` if child cursor in file, else `False`
:rtype: bool
"""
return self.is_cursor_in_file(child_cursor, self.filename)
return self.is_cursor_in_files(
child_cursor, [self.filename, *self.inclusion_sources]
)

def _construct_tree(self, node):
"""Recursively generates tree by traversing the AST of the node.
Expand Down