"""This tool extracts information about structs and defines from the C headers.
The JSON input format is as follows:
[
{
'file': 'some/header.h',
'structs': {
'struct_name': [
'field1',
'field2',
'field3',
{
'field4': [
'nested1',
'nested2',
{
'nested3': [
'deep_nested1',
...
]
}
...
]
},
'field5'
],
'other_struct': [
'field1',
'field2',
...
]
},
'defines': [
'DEFINE_1',
'DEFINE_2',
['f', 'FLOAT_DEFINE'],
'DEFINE_3',
...
]
},
{
'file': 'some/other/header.h',
...
}
]
Please note that the 'f' for 'FLOAT_DEFINE' is just the format passed to printf(), you can put
anything printf() understands.
"""
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
__scriptdir__ = os.path.dirname(os.path.abspath(__file__))
__rootdir__ = os.path.dirname(__scriptdir__)
sys.path.insert(0, __rootdir__)
from tools import config, shared, system_libs, utils
QUIET = (__name__ != '__main__')
DEBUG = False
CFLAGS = [
'-D_GNU_SOURCE',
]
INTERNAL_CFLAGS = [
'-I' + utils.path_from_root('system/lib/libc/musl/src/internal'),
'-I' + utils.path_from_root('system/lib/libc/musl/src/include'),
'-I' + utils.path_from_root('system/lib/pthread/'),
]
CXXFLAGS = [
'-I' + utils.path_from_root('system/lib/libcxxabi/src'),
'-D__EMSCRIPTEN_EXCEPTIONS__',
'-I' + utils.path_from_root('system/lib/wasmfs/'),
]
DEFAULT_JSON_FILES = [
utils.path_from_root('src/struct_info.json'),
utils.path_from_root('src/struct_info_internal.json'),
utils.path_from_root('src/struct_info_cxx.json'),
]
def show(msg, *args):
if shared.DEBUG or not QUIET:
print('gen_struct_info:', msg, *args, file=sys.stderr)
class Scope:
def __init__(self, code: list[str]):
self.code = code
self.has_data = False
def __enter__(self):
self.code.append('puts("{");')
return self
def __exit__(self, _exc_type, _exc_val, _exc_tb):
if self.has_data:
self.code.append('puts("");')
self.code.append('printf("}");')
def _start_child(self, name: str):
if self.has_data:
self.code.append('puts(",");')
else:
self.has_data = True
if '::' in name:
name = name.split('::', 1)[1]
self.code.append(fr'printf("\"{name}\": ");')
def child(self, name: str):
self._start_child(name)
return Scope(self.code)
def set(self, name: str, type_: str, value: str):
self._start_child(name)
assert type_.startswith('%')
assert type_[-1] in {'d', 'i', 'u', 'f', 'F', 'e', 'E'}
self.code.append(f'printf("{type_}", {value});')
def gen_inspect_code(self, path: list[str], struct: list[str | dict]):
if path[0][-1] == '#':
path[0] = path[0].rstrip('#')
prefix = ''
else:
prefix = 'struct '
prefix += path[0]
with self.child(path[-1]) as scope:
path_for_sizeof = [f'({prefix}){{}}'] + path[1:]
scope.set('__size__', '%zu', f'sizeof ({".".join(path_for_sizeof)})')
for field in struct:
if isinstance(field, dict):
fname = list(field.keys())[0]
self.gen_inspect_code(path + [fname], field[fname])
else:
scope.set(field, '%zu', f'offsetof({prefix}, {".".join(path[1:] + [field])})')
def generate_c_code(headers):
code = ['#include <stdio.h>', '#include <stddef.h>']
code.extend(f'''#include "{header['name']}"''' for header in headers)
code.append('int main() {')
with Scope(code) as root:
with root.child('structs') as structs:
for header in headers:
for name, struct in header['structs'].items():
structs.gen_inspect_code([name], struct)
with root.child('defines') as defines:
for header in headers:
for name, type_ in header['defines'].items():
if '%' not in type_:
type_ = f'%{type_}'
defines.set(name, type_, name)
code.append('puts("");')
code.append('return 0;')
code.append('}')
return code
def generate_cmd(js_file_path, src_file_path, cflags):
show('Compiling generated code...')
if any('libcxxabi' in f for f in cflags):
compiler = shared.EMXX
else:
compiler = shared.EMCC
cmd = [compiler] + cflags + ['-o', js_file_path, src_file_path,
'-O0',
'-Werror',
'-Wno-format',
'-sBOOTSTRAPPING_STRUCT_INFO',
'-sWASM_ASYNC_COMPILATION=0',
'-sINCOMING_MODULE_JS_API=',
'-sSTRICT',
'-sSUPPORT_LONGJMP=0',
'-sASSERTIONS=0']
cmd += ['-Wno-error=version-check']
cmd += ['-Wno-deprecated']
show(shlex.join(cmd))
return cmd
def inspect_headers(headers, cflags):
src_file_fd, src_file_path = tempfile.mkstemp('.c', text=True)
show('Generating C code... ' + src_file_path)
code = generate_c_code(headers)
os.write(src_file_fd, '\n'.join(code).encode())
os.close(src_file_fd)
js_file_fd, js_file_path = tempfile.mkstemp('.js')
os.close(js_file_fd)
cmd = generate_cmd(js_file_path, src_file_path, cflags)
try:
subprocess.check_call(cmd, env=system_libs.clean_env())
except subprocess.CalledProcessError as e:
sys.stderr.write('FAIL: Compilation failed!: %s\n' % e.cmd)
sys.exit(1)
show('Running generated program... ' + js_file_path, config.NODE_JS)
info = shared.run_js_tool(js_file_path, stdout=shared.PIPE)
if not DEBUG:
os.unlink(src_file_path)
if os.path.exists(js_file_path):
os.unlink(js_file_path)
wasm_file_path = utils.replace_suffix(js_file_path, '.wasm')
os.unlink(wasm_file_path)
return json.loads(info)
def merge_info(target, src):
for key, value in src['defines'].items():
if key in target['defines']:
raise Exception('duplicate define: %s' % key)
target['defines'][key] = value
for key, value in src['structs'].items():
if key in target['structs']:
raise Exception('duplicate struct: %s' % key)
target['structs'][key] = value
def inspect_code(headers, cflags):
if not DEBUG:
info = inspect_headers(headers, cflags)
else:
info = {'defines': {}, 'structs': {}}
for header in headers:
merge_info(info, inspect_headers([header], cflags))
return info
def parse_json(path):
header_files = []
with open(path) as stream:
data = json.loads(re.sub(r'//.*\n', '', stream.read()))
if not isinstance(data, list):
data = [data]
for item in data:
for key in item:
if key not in ['file', 'defines', 'structs']:
raise 'Unexpected key in json file: %s' % key
header = {'name': item['file'], 'structs': {}, 'defines': {}}
for name, data in item.get('structs', {}).items():
if name in header['structs']:
show('WARN: Description of struct "' + name + '" in file "' + item['file'] + '" replaces an existing description!')
header['structs'][name] = data
for part in item.get('defines', []):
if not isinstance(part, list):
part = ['i', part]
if part[1] in header['defines']:
show('WARN: Description of define "' + part[1] + '" in file "' + item['file'] + '" replaces an existing description!')
header['defines'][part[1]] = part[0]
header_files.append(header)
return header_files
def output_json(obj, stream):
json.dump(obj, stream, indent=4, sort_keys=True)
stream.write('\n')
stream.close()
def main(args):
global QUIET
parser = argparse.ArgumentParser(description='Generate JSON infos for structs.')
parser.add_argument('json', nargs='*',
help='JSON file with a list of structs and their fields (defaults to src/struct_info.json)',
default=DEFAULT_JSON_FILES)
parser.add_argument('-q', dest='quiet', action='store_true', default=False,
help='Don\'t output anything besides error messages.')
parser.add_argument('-o', dest='output', metavar='path', default=None,
help='Path to the JSON file that will be written. If omitted, the default location under `src` will be used.')
parser.add_argument('-I', dest='includes', metavar='dir', action='append', default=[],
help='Add directory to include search path')
parser.add_argument('-D', dest='defines', metavar='define', action='append', default=[],
help='Pass a define to the preprocessor')
parser.add_argument('-U', dest='undefines', metavar='undefine', action='append', default=[],
help='Pass an undefine to the preprocessor')
parser.add_argument('--wasm64', action='store_true',
help='use wasm64 architecture')
args = parser.parse_args(args)
QUIET = args.quiet
extra_cflags = []
if args.wasm64:
extra_cflags += ['-sMEMORY64=2']
for path in args.includes:
extra_cflags.append('-I' + path)
for arg in args.defines:
extra_cflags.append('-D' + arg)
for arg in args.undefines:
extra_cflags.append('-U' + arg)
info = {'defines': {}, 'structs': {}}
for f in args.json:
header_files = parse_json(f)
if 'internal' in f:
use_cflags = CFLAGS + extra_cflags + INTERNAL_CFLAGS
elif 'cxx' in f:
use_cflags = CFLAGS + extra_cflags + CXXFLAGS
else:
use_cflags = CFLAGS + extra_cflags
info_fragment = inspect_code(header_files, use_cflags)
merge_info(info, info_fragment)
if args.output:
output_file = args.output
elif args.wasm64:
output_file = utils.path_from_root('src/struct_info_generated_wasm64.json')
else:
output_file = utils.path_from_root('src/struct_info_generated.json')
with open(output_file, 'w') as f:
output_json(info, f)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))