Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/gen_struct_info.py
4128 views
1
#!/usr/bin/env python3
2
# Copyright 2013 The Emscripten Authors. All rights reserved.
3
# Emscripten is available under two separate licenses, the MIT license and the
4
# University of Illinois/NCSA Open Source License. Both these licenses can be
5
# found in the LICENSE file.
6
7
"""This tool extracts information about structs and defines from the C headers.
8
9
The JSON input format is as follows:
10
[
11
{
12
'file': 'some/header.h',
13
'structs': {
14
'struct_name': [
15
'field1',
16
'field2',
17
'field3',
18
{
19
'field4': [
20
'nested1',
21
'nested2',
22
{
23
'nested3': [
24
'deep_nested1',
25
...
26
]
27
}
28
...
29
]
30
},
31
'field5'
32
],
33
'other_struct': [
34
'field1',
35
'field2',
36
...
37
]
38
},
39
'defines': [
40
'DEFINE_1',
41
'DEFINE_2',
42
['f', 'FLOAT_DEFINE'],
43
'DEFINE_3',
44
...
45
]
46
},
47
{
48
'file': 'some/other/header.h',
49
...
50
}
51
]
52
53
Please note that the 'f' for 'FLOAT_DEFINE' is just the format passed to printf(), you can put
54
anything printf() understands.
55
"""
56
57
import sys
58
import os
59
import re
60
import json
61
import argparse
62
import tempfile
63
import shlex
64
import subprocess
65
import typing
66
67
__scriptdir__ = os.path.dirname(os.path.abspath(__file__))
68
__rootdir__ = os.path.dirname(__scriptdir__)
69
sys.path.insert(0, __rootdir__)
70
71
from tools import building
72
from tools import shared
73
from tools import system_libs
74
from tools import utils
75
76
QUIET = (__name__ != '__main__')
77
DEBUG = False
78
79
CFLAGS = [
80
# Avoid parsing problems due to gcc specific syntax.
81
'-D_GNU_SOURCE',
82
]
83
84
INTERNAL_CFLAGS = [
85
'-I' + utils.path_from_root('system/lib/libc/musl/src/internal'),
86
'-I' + utils.path_from_root('system/lib/libc/musl/src/include'),
87
'-I' + utils.path_from_root('system/lib/pthread/'),
88
]
89
90
CXXFLAGS = [
91
'-I' + utils.path_from_root('system/lib/libcxxabi/src'),
92
'-D__EMSCRIPTEN_EXCEPTIONS__',
93
'-I' + utils.path_from_root('system/lib/wasmfs/'),
94
'-std=c++17',
95
]
96
97
DEFAULT_JSON_FILES = [
98
utils.path_from_root('src/struct_info.json'),
99
utils.path_from_root('src/struct_info_internal.json'),
100
utils.path_from_root('src/struct_info_cxx.json'),
101
utils.path_from_root('src/struct_info_webgpu.json'),
102
]
103
104
105
def show(msg):
106
if shared.DEBUG or not QUIET:
107
sys.stderr.write('gen_struct_info: %s\n' % msg)
108
109
110
# The Scope class generates C code which, in turn, outputs JSON.
111
#
112
# Example:
113
# with Scope(code) as scope: # generates code that outputs beginning of a JSON object '{\n'
114
# scope.set('item', '%i', '111') # generates code that outputs '"item": 111'
115
# scope.set('item2', '%f', '4.2') # generates code that outputs ',\n"item2": 4.2'
116
# # once the scope is exited, it generates code that outputs the end of the JSON object '\n}'
117
class Scope:
118
def __init__(self, code: typing.List[str]):
119
self.code = code
120
self.has_data = False
121
122
def __enter__(self):
123
self.code.append('puts("{");')
124
return self
125
126
def __exit__(self, exc_type, exc_val, exc_tb):
127
if self.has_data:
128
self.code.append('puts("");')
129
self.code.append('printf("}");')
130
131
def _start_child(self, name: str):
132
if self.has_data:
133
self.code.append('puts(",");')
134
else:
135
self.has_data = True
136
if '::' in name:
137
name = name.split('::', 1)[1]
138
self.code.append(fr'printf("\"{name}\": ");')
139
140
def child(self, name: str):
141
self._start_child(name)
142
return Scope(self.code)
143
144
def set(self, name: str, type_: str, value: str):
145
self._start_child(name)
146
147
assert type_.startswith('%')
148
# We only support numeric defines as they are directly compatible with JSON.
149
# Extend to string escaping if we ever need that in the future.
150
assert type_[-1] in {'d', 'i', 'u', 'f', 'F', 'e', 'E'}
151
152
self.code.append(f'printf("{type_}", {value});')
153
154
def gen_inspect_code(self, path: typing.List[str], struct: typing.List[typing.Union[str, dict]]):
155
if path[0][-1] == '#':
156
path[0] = path[0].rstrip('#')
157
prefix = ''
158
else:
159
prefix = 'struct '
160
prefix += path[0]
161
162
with self.child(path[-1]) as scope:
163
path_for_sizeof = [f'({prefix}){{}}'] + path[1:]
164
scope.set('__size__', '%zu', f'sizeof ({".".join(path_for_sizeof)})')
165
166
for field in struct:
167
if isinstance(field, dict):
168
# We have to recurse to inspect the nested dict.
169
fname = list(field.keys())[0]
170
self.gen_inspect_code(path + [fname], field[fname])
171
else:
172
scope.set(field, '%zu', f'offsetof({prefix}, {".".join(path[1:] + [field])})')
173
174
175
def generate_c_code(headers):
176
code = ['#include <stdio.h>', '#include <stddef.h>']
177
178
code.extend(f'''#include "{header['name']}"''' for header in headers)
179
180
code.append('int main() {')
181
182
with Scope(code) as root:
183
with root.child('structs') as structs:
184
for header in headers:
185
for name, struct in header['structs'].items():
186
structs.gen_inspect_code([name], struct)
187
188
with root.child('defines') as defines:
189
for header in headers:
190
for name, type_ in header['defines'].items():
191
# Add the necessary python type, if missing.
192
if '%' not in type_:
193
type_ = f'%{type_}'
194
195
defines.set(name, type_, name)
196
197
code.append('puts("");') # Add a newline after the JSON output to flush it.
198
199
code.append('return 0;')
200
code.append('}')
201
202
return code
203
204
205
def generate_cmd(js_file_path, src_file_path, cflags):
206
# Compile the program.
207
show('Compiling generated code...')
208
209
if any('libcxxabi' in f for f in cflags):
210
compiler = shared.EMXX
211
else:
212
compiler = shared.EMCC
213
214
node_flags = building.get_emcc_node_flags(shared.check_node_version())
215
216
# -O1+ produces calls to iprintf, which libcompiler_rt doesn't support
217
cmd = [compiler] + cflags + ['-o', js_file_path, src_file_path,
218
'-O0',
219
'-Werror',
220
'-Wno-format',
221
'-sBOOTSTRAPPING_STRUCT_INFO',
222
'-sWASM_ASYNC_COMPILATION=0',
223
'-sINCOMING_MODULE_JS_API=',
224
'-sSTRICT',
225
'-sSUPPORT_LONGJMP=0',
226
'-sASSERTIONS=0'] + node_flags
227
228
# Default behavior for emcc is to warn for binaryen version check mismatches
229
# so we should try to match that behavior.
230
cmd += ['-Wno-error=version-check']
231
232
# TODO(sbc): Remove this one we remove the test_em_config_env_var test
233
cmd += ['-Wno-deprecated']
234
235
show(shlex.join(cmd))
236
return cmd
237
238
239
def inspect_headers(headers, cflags):
240
# Write the source code to a temporary file.
241
src_file_fd, src_file_path = tempfile.mkstemp('.c', text=True)
242
show('Generating C code... ' + src_file_path)
243
code = generate_c_code(headers)
244
os.write(src_file_fd, '\n'.join(code).encode())
245
os.close(src_file_fd)
246
247
js_file_fd, js_file_path = tempfile.mkstemp('.js')
248
# Close the unneeded FD.
249
os.close(js_file_fd)
250
251
cmd = generate_cmd(js_file_path, src_file_path, cflags)
252
253
try:
254
subprocess.check_call(cmd, env=system_libs.clean_env())
255
except subprocess.CalledProcessError as e:
256
sys.stderr.write('FAIL: Compilation failed!: %s\n' % e.cmd)
257
sys.exit(1)
258
259
# Run the compiled program.
260
show('Calling generated program... ' + js_file_path)
261
info = shared.run_js_tool(js_file_path, stdout=shared.PIPE)
262
263
if not DEBUG:
264
# Remove all temporary files.
265
os.unlink(src_file_path)
266
267
if os.path.exists(js_file_path):
268
os.unlink(js_file_path)
269
wasm_file_path = shared.replace_suffix(js_file_path, '.wasm')
270
os.unlink(wasm_file_path)
271
272
# Parse the output of the program into a dict.
273
return json.loads(info)
274
275
276
def merge_info(target, src):
277
for key, value in src['defines'].items():
278
if key in target['defines']:
279
raise Exception('duplicate define: %s' % key)
280
target['defines'][key] = value
281
282
for key, value in src['structs'].items():
283
if key in target['structs']:
284
raise Exception('duplicate struct: %s' % key)
285
target['structs'][key] = value
286
287
288
def inspect_code(headers, cflags):
289
if not DEBUG:
290
info = inspect_headers(headers, cflags)
291
else:
292
info = {'defines': {}, 'structs': {}}
293
for header in headers:
294
merge_info(info, inspect_headers([header], cflags))
295
return info
296
297
298
def parse_json(path):
299
header_files = []
300
301
with open(path) as stream:
302
# Remove comments before loading the JSON.
303
data = json.loads(re.sub(r'//.*\n', '', stream.read()))
304
305
if not isinstance(data, list):
306
data = [data]
307
308
for item in data:
309
for key in item:
310
if key not in ['file', 'defines', 'structs']:
311
raise 'Unexpected key in json file: %s' % key
312
313
header = {'name': item['file'], 'structs': {}, 'defines': {}}
314
for name, data in item.get('structs', {}).items():
315
if name in header['structs']:
316
show('WARN: Description of struct "' + name + '" in file "' + item['file'] + '" replaces an existing description!')
317
318
header['structs'][name] = data
319
320
for part in item.get('defines', []):
321
if not isinstance(part, list):
322
# If no type is specified, assume integer.
323
part = ['i', part]
324
325
if part[1] in header['defines']:
326
show('WARN: Description of define "' + part[1] + '" in file "' + item['file'] + '" replaces an existing description!')
327
328
header['defines'][part[1]] = part[0]
329
330
header_files.append(header)
331
332
return header_files
333
334
335
def output_json(obj, stream):
336
json.dump(obj, stream, indent=4, sort_keys=True)
337
stream.write('\n')
338
stream.close()
339
340
341
def main(args):
342
global QUIET
343
344
parser = argparse.ArgumentParser(description='Generate JSON infos for structs.')
345
parser.add_argument('json', nargs='*',
346
help='JSON file with a list of structs and their fields (defaults to src/struct_info.json)',
347
default=DEFAULT_JSON_FILES)
348
parser.add_argument('-q', dest='quiet', action='store_true', default=False,
349
help='Don\'t output anything besides error messages.')
350
parser.add_argument('-o', dest='output', metavar='path', default=None,
351
help='Path to the JSON file that will be written. If omitted, the default location under `src` will be used.')
352
parser.add_argument('-I', dest='includes', metavar='dir', action='append', default=[],
353
help='Add directory to include search path')
354
parser.add_argument('-D', dest='defines', metavar='define', action='append', default=[],
355
help='Pass a define to the preprocessor')
356
parser.add_argument('-U', dest='undefines', metavar='undefine', action='append', default=[],
357
help='Pass an undefine to the preprocessor')
358
parser.add_argument('--wasm64', action='store_true',
359
help='use wasm64 architecture')
360
args = parser.parse_args(args)
361
362
QUIET = args.quiet
363
364
extra_cflags = []
365
366
if args.wasm64:
367
# Always use =2 here so that we don't generate a binary that actually requires
368
# memory64 to run. All we care about is that the output is correct.
369
extra_cflags += ['-sMEMORY64=2', '-Wno-experimental']
370
371
# Add the user options to the list as well.
372
for path in args.includes:
373
extra_cflags.append('-I' + path)
374
375
for arg in args.defines:
376
extra_cflags.append('-D' + arg)
377
378
for arg in args.undefines:
379
extra_cflags.append('-U' + arg)
380
381
# Look for structs in all passed headers.
382
info = {'defines': {}, 'structs': {}}
383
384
for f in args.json:
385
# This is a JSON file, parse it.
386
header_files = parse_json(f)
387
# Inspect all collected structs.
388
if 'internal' in f:
389
use_cflags = CFLAGS + extra_cflags + INTERNAL_CFLAGS
390
elif 'cxx' in f:
391
use_cflags = CFLAGS + extra_cflags + CXXFLAGS
392
else:
393
use_cflags = CFLAGS + extra_cflags
394
info_fragment = inspect_code(header_files, use_cflags)
395
merge_info(info, info_fragment)
396
397
if args.output:
398
output_file = args.output
399
elif args.wasm64:
400
output_file = utils.path_from_root('src/struct_info_generated_wasm64.json')
401
else:
402
output_file = utils.path_from_root('src/struct_info_generated.json')
403
404
with open(output_file, 'w') as f:
405
output_json(info, f)
406
407
return 0
408
409
410
if __name__ == '__main__':
411
sys.exit(main(sys.argv[1:]))
412
413