-
Notifications
You must be signed in to change notification settings - Fork 138
/
Copy pathpython_wrapper_helper.py.in
284 lines (242 loc) · 13 KB
/
python_wrapper_helper.py.in
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#!/usr/bin/python
# ----------------- BEGIN LICENSE BLOCK ---------------------------------
#
# Copyright (c) 2019-2022 Intel Corporation
#
# ----------------- END LICENSE BLOCK -----------------------------------
import os
import re
import sys
from pygccxml import parser
from pygccxml import utils
from pyplusplus import module_builder, decl_wrappers
import fileinput
import warnings
def get_list_of_files(directory, ignore_files):
"""
@return list of all files in directory and subdirectories of directory
"""
dir_list = os.listdir(directory)
all_files = list()
for entry in dir_list:
full_path = os.path.join(directory, entry)
if os.path.isdir(full_path):
all_files = all_files + get_list_of_files(full_path, ignore_files)
else:
skip = False
for ignore_file in ignore_files:
if full_path.find(ignore_file) != -1:
skip = True
print("Skipping file: " + full_path)
if not skip:
if full_path.endswith(".h") or full_path.endswith(".hpp"):
all_files.append(full_path)
return all_files
def generate_python_wrapper(header_directories, include_paths, library_name, cpp_filename, declarations, main_namespace="", ignore_declarations={}, ignore_files={}, add_declarations={}):
"""
Function to generate Python-C++ binding code by calling pygccxml and py++
:param header_directories: directories with headers to create python binding for
:type header_directories: list<string>
:param include_paths: directories required as includes for the header files
:type include_paths: list<string>
:param library_name: the output name of the library to be created
:type library_name: string
:param cpp_filename: the output name of the C++ file to be created
:type cpp_filename: string
:param declarations: a list of declarations to be used for starting the AST syntax tree. See also
pygccxml start_with_declarations or either -fxml-start(gccxml) or -castxml-start(castxml)
:type declarations: list<string>
:param ignore_declarations: a list of declarations to be ignored when generating C++ code
:type ignore_declarations: list<string>
:param ignore_files: a list of files to be ignored
:type ignore_files: list<string>
:param add_declarations: a list of declarations to be explicitly enabled searched for in
against the full declaration string resulting from '"{}".format(decl)' output
(useful e.g. for typedefs to template types which are not as such visible in the delcarations)
:type add_declarations: list<string>
:return:
"""
warnings.filterwarnings(action="once", category=DeprecationWarning)
# Find out the xml generator (gccxml or castxml)
generator_path, generator_name = utils.find_xml_generator()
compiler = "@CXX@"
# Create configuration for CastXML
xml_generator_config = parser.xml_generator_configuration_t(
xml_generator_path=generator_path,
xml_generator=generator_name,
compiler=compiler,
start_with_declarations=declarations)
# Set include dirs and cflags to avoid warnings and errors
xml_generator_config.append_cflags("-std=c++@CMAKE_CXX_STANDARD@")
xml_generator_config.append_cflags("-Wno-error=invalid-constexpr")
xml_generator_config.append_cflags("-DSAFE_DATATYPES_EXPLICIT_CONVERSION=1")
for inc_dir in include_paths:
xml_generator_config.include_paths.append(inc_dir)
for header_dir in header_directories:
xml_generator_config.include_paths.append(header_dir)
# Find all relevant headers
header_list = list()
for header_dir in header_directories:
header_list = header_list + get_list_of_files(header_dir, ignore_files=ignore_files)
# Parses the source files and creates a module_builder object
builder = module_builder.module_builder_t(
header_list,
xml_generator_path=generator_path,
compilation_mode=parser.COMPILATION_MODE.ALL_AT_ONCE,
xml_generator_config=xml_generator_config,
indexing_suite_version=2)
# for some reason there is a problem with variables named 'length'
# the filename is empty and the line no is set to -1
# therefore the declaration is not processed in the code later on
# this is to fix length struct members
for decl in builder.decls("length"):
if isinstance(decl, decl_wrappers.variable_wrapper.variable_t):
if isinstance(decl.parent, decl_wrappers.class_wrapper.class_t):
if decl.ignore:
decl.location.file_name = decl.parent.location.file_name
decl.location.line = decl.parent.location.line + 1
decl.ignore = False
if main_namespace != "":
if main_namespace.startswith("::"):
main_namespace = main_namespace[2:]
top_namespace_split = main_namespace.find(":")
top_namespace = ""
if top_namespace_split > 1:
top_namespace = main_namespace[:top_namespace_split + 2]
print("Main namespace defined, namespace filtering enabled: top-namespace '{}' main-namespace '{}'".format(
top_namespace, main_namespace))
for decl in builder.decls():
decl_full_string_and_type = "{}".format(decl)
# print("declaration {} (alias {}, full {})".format(decl.name, decl.alias, decl_full_string_and_type))
if main_namespace != "":
if isinstance(decl, decl_wrappers.class_wrapper.class_t) or isinstance(decl, decl_wrappers.class_wrapper.class_declaration_t) or isinstance(decl, decl_wrappers.typedef_wrapper.typedef_t) or isinstance(decl, decl_wrappers.enumeration_wrapper.enumeration_t):
decl_full_string = decl_full_string_and_type.split(" ")[0]
if main_namespace in decl_full_string:
# namespace present, ok
# print("typedef/class/enum main namespace found [ignore-{},exposed-{}]:
# {}".format(decl.ignore, decl.already_exposed,
# decl_full_string_and_type))
pass
elif decl_full_string in main_namespace:
# declaration is a parent of main namespace, ok
# print("typedef/class/enum declaration of upper level namespace found
# [ignore{},exposed{}]: {}".format(decl.ignore, decl.already_exposed,
# decl_full_string_and_type))
pass
elif top_namespace != "" and not top_namespace in decl_full_string:
# global or std defaults, ok
# print("typedef/class/enum outside top namespace found
# [ignore-{},exposed-{}]: {}".format(decl.ignore, decl.already_exposed,
# decl_full_string_and_type))
pass
else:
# print("typedef/class/enum outside of main namespace found
# [ignore-{},exposed-{}]. Ignoring: {}".format(decl.ignore,
# decl.already_exposed, decl_full_string_and_type))
decl.exclude()
decl.ignore = True
# try to enforce that it's not exported anymore
decl.already_exposed = True
if decl.ignore:
if decl_full_string in declarations:
decl.ignore = False
decl.already_exposed = False
# print("Ignored typedef/class/enum explicitly listed in delclarations
# found. Reenable {}".format(decl_full_string_and_type))
for add_declaration in add_declarations:
if (add_declaration in decl.name) or (add_declaration in decl.alias) or (add_declaration in decl_full_string_and_type):
decl.ignore = False
decl.already_exposed = False
# print("declaration to explicitly add found: {} (alias {})".format(decl, decl.alias))
break
for ignore_declaration in ignore_declarations:
if (ignore_declaration in decl.name) or (ignore_declaration in decl.alias) or (ignore_declaration in decl_full_string_and_type):
decl.exclude()
decl.ignore = True
decl.already_exposed = True
# print("declaration to ignore found: {} (alias {})".format(decl, decl.alias))
break
# debug declarations
# builder.print_declarations()
# Automatically detect properties and associated getters/setters
builder.classes().add_properties(exclude_accessors=True)
# Define a name for the module
builder.build_code_creator(module_name=library_name)
# Writes the C++ interface file
if os.path.exists(cpp_filename):
os.remove(cpp_filename)
builder.write_module(cpp_filename)
print("generate_python_wrapper(): {} written.".format(cpp_filename))
def post_process_python_wrapper(header_directories, cpp_filename_in, cpp_filename_out, additional_replacements=[], additional_includes={}, spdx_license="MIT", fix_include_directives=True, fix_enum_class=True):
"""
Post process generated binding code
As the generated code is not 100% perfect, some additional steps are automated by these post processing steps.
1. The auto-generated include directives contain the full include path which is removed in here
2. enum literals of C++ enum classes are not correctly handled by the generator, this fixes this
:param header_directories: directories with headers the python binding is created for; used to remove the prefix from
generated include directives
:type header_directories: list<string>
:param cpp_filename_in: the input name of the C++ file to be post processed
:type cpp_filename_in: string
:param cpp_filename_out: the output name of the C++ file to be post processed
:type cpp_filename_out: string
:param fix_include_directives: should the include directives be fixed (default: True)
:type fix_include_directives: boolean
:param fix_enum_class: should the enum classes be fixed (default: True)
:type fix_enum_class: boolean
:return:
"""
enum_declaration_start = re.compile(".*bp::enum_< ([^>]*)>.*")
enum_namespace = ""
enum_namespace_full = ""
enum_started = False
file_input = fileinput.input(cpp_filename_in)
file_output = open(cpp_filename_out, "w")
write_prefix = True
for line in file_input:
if write_prefix:
file_output.write("/*\n"
" * ----------------- BEGIN LICENSE BLOCK ---------------------------------\n"
" *\n"
" * Copyright (c) 2020-2021 Intel Corporation\n"
" *\n"
" * SPDX-License-Identifier: " + spdx_license + "\n"
" *\n"
" * ----------------- END LICENSE BLOCK -----------------------------------\n"
" */\n"
"// clang-format off\n"
"#pragma GCC diagnostic push\n"
"#pragma GCC diagnostic ignored \"-Wshadow\"\n"
"#pragma GCC diagnostic ignored \"-Wignored-qualifiers\"\n"
"#if defined(__clang__) && (__clang_major__ >= 7)\n"
"# pragma GCC diagnostic ignored \"-Wself-assign-overloaded\"\n"
"#endif\n\n")
for additional_include in additional_includes:
file_output.write("#include <{}>\n".format(additional_include))
write_prefix = False
# Remove the leading include
if fix_include_directives:
for header_dir in header_directories:
if not header_dir.endswith("/"):
header_dir = header_dir + "/"
line = str.replace(line, header_dir, "")
# Fix C++ enum classes
if fix_enum_class:
if enum_started:
if line.find("export_values()") != -1:
enum_started = False
else:
line = str.replace(line, enum_namespace, enum_namespace_full)
else:
match = enum_declaration_start.match(line)
if match:
enum_started = True
enum_name_start = match.group(1).rindex(':')
enum_namespace = match.group(1)[:enum_name_start + 1]
enum_namespace_full = match.group(1) + "::"
for replacement in additional_replacements:
line = str.replace(line, replacement[0], replacement[1])
file_output.write(line)
file_output.write("\n#pragma GCC diagnostic pop\n"
"// clang-format on\n")
print("post_process_python_wrapper(): {} written.".format(cpp_filename_out))