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