"""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 sys
import os
import re
import json
import argparse
import tempfile
import shlex
import subprocess
import typing
__scriptdir__ = os.path.dirname(os.path.abspath(__file__))
__rootdir__ = os.path.dirname(__scriptdir__)
sys.path.insert(0, __rootdir__)
from tools import building
from tools import shared
from tools import system_libs
from tools import 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/'),
'-std=c++17',
]
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'),
utils.path_from_root('src/struct_info_webgpu.json'),
]
def show(msg):
if shared.DEBUG or not QUIET:
sys.stderr.write('gen_struct_info: %s\n' % msg)
class Scope:
def __init__(self, code: typing.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: typing.List[str], struct: typing.List[typing.Union[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
node_flags = building.get_emcc_node_flags(shared.check_node_version())
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'] + node_flags
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('Calling generated program... ' + js_file_path)
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 = shared.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', '-Wno-experimental']
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:]))