Skip to content

Commit

Permalink
bindgen: Initial doc comments for Odin
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexanderArvidsson committed Jan 5, 2025
1 parent 789d970 commit 543bd50
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 31 deletions.
88 changes: 58 additions & 30 deletions bindgen/gen_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ def is_api_decl(decl, prefix):
return decl['name'].startswith(prefix)
elif decl['kind'] == 'EnumDecl':
# an anonymous enum, check if the items start with the prefix
return decl['inner'][0]['name'].lower().startswith(prefix)
first = get_first_non_comment(decl['inner'])
return first['name'].lower().startswith(prefix)
else:
return False

def get_first_non_comment(items):
return next(i for i in items if i['kind'] != 'FullComment')

def strip_comments(items):
return [i for i in items if i['kind'] != 'FullComment']

def extract_comment(comment, source):
return source[comment['range']['begin']['offset']:comment['range']['end']['offset']]

def is_dep_decl(decl, dep_prefixes):
for prefix in dep_prefixes:
if is_api_decl(decl, prefix):
Expand All @@ -27,12 +37,16 @@ def dep_prefix(decl, dep_prefixes):
def filter_types(str):
return str.replace('_Bool', 'bool')

def parse_struct(decl):
def parse_struct(decl, source):
outp = {}
outp['kind'] = 'struct'
outp['name'] = decl['name']
outp['fields'] = []
for item_decl in decl['inner']:
if item_decl['kind'] == 'FullComment':
outp['comment'] = extract_comment(item_decl, source)
outp['comment_multiline'] = '\n' in outp['comment']
continue
if item_decl['kind'] != 'FieldDecl':
sys.exit(f"ERROR: Structs must only contain simple fields ({decl['name']})")
item = {}
Expand All @@ -42,7 +56,7 @@ def parse_struct(decl):
outp['fields'].append(item)
return outp

def parse_enum(decl):
def parse_enum(decl, source):
outp = {}
if 'name' in decl:
outp['kind'] = 'enum'
Expand All @@ -53,31 +67,42 @@ def parse_enum(decl):
needs_value = True
outp['items'] = []
for item_decl in decl['inner']:
if item_decl['kind'] == 'FullComment':
outp['comment'] = extract_comment(item_decl, source)
outp['comment_multiline'] = '\n' in outp['comment']
continue
if item_decl['kind'] == 'EnumConstantDecl':
item = {}
item['name'] = item_decl['name']
if 'inner' in item_decl:
const_expr = item_decl['inner'][0]
if const_expr['kind'] != 'ConstantExpr':
sys.exit(f"ERROR: Enum values must be a ConstantExpr ({item_decl['name']}), is '{const_expr['kind']}'")
if const_expr['valueCategory'] != 'rvalue' and const_expr['valueCategory'] != 'prvalue':
sys.exit(f"ERROR: Enum value ConstantExpr must be 'rvalue' or 'prvalue' ({item_decl['name']}), is '{const_expr['valueCategory']}'")
if not ((len(const_expr['inner']) == 1) and (const_expr['inner'][0]['kind'] == 'IntegerLiteral')):
sys.exit(f"ERROR: Enum value ConstantExpr must have exactly one IntegerLiteral ({item_decl['name']})")
item['value'] = const_expr['inner'][0]['value']
exprs = strip_comments(item_decl['inner'])
if len(exprs) > 0:
const_expr = exprs[0]
if const_expr['kind'] != 'ConstantExpr':
sys.exit(f"ERROR: Enum values must be a ConstantExpr ({item_decl['name']}), is '{const_expr['kind']}'")
if const_expr['valueCategory'] != 'rvalue' and const_expr['valueCategory'] != 'prvalue':
sys.exit(f"ERROR: Enum value ConstantExpr must be 'rvalue' or 'prvalue' ({item_decl['name']}), is '{const_expr['valueCategory']}'")
const_expr_inner = strip_comments(const_expr['inner'])
if not ((len(const_expr_inner) == 1) and (const_expr_inner[0]['kind'] == 'IntegerLiteral')):
sys.exit(f"ERROR: Enum value ConstantExpr must have exactly one IntegerLiteral ({item_decl['name']})")
item['value'] = const_expr_inner[0]['value']
if needs_value and 'value' not in item:
sys.exit(f"ERROR: anonymous enum items require an explicit value")
sys.exit("ERROR: anonymous enum items require an explicit value")
outp['items'].append(item)
return outp

def parse_func(decl):
def parse_func(decl, source):
outp = {}
outp['kind'] = 'func'
outp['name'] = decl['name']
outp['type'] = filter_types(decl['type']['qualType'])
outp['params'] = []
if 'inner' in decl:
for param in decl['inner']:
if param['kind'] == 'FullComment':
outp['comment'] = extract_comment(param, source)
outp['comment_multiline'] = '\n' in outp['comment']
continue
if param['kind'] != 'ParmVarDecl':
print(f" >> warning: ignoring func {decl['name']} (unsupported parameter type)")
return None
Expand All @@ -87,38 +112,41 @@ def parse_func(decl):
outp['params'].append(outp_param)
return outp

def parse_decl(decl):
def parse_decl(decl, source):
kind = decl['kind']
if kind == 'RecordDecl':
return parse_struct(decl)
return parse_struct(decl, source)
elif kind == 'EnumDecl':
return parse_enum(decl)
return parse_enum(decl, source)
elif kind == 'FunctionDecl':
return parse_func(decl)
return parse_func(decl, source)
else:
return None

def clang(csrc_path):
cmd = ['clang', '-Xclang', '-ast-dump=json', '-c' ]
cmd.append(csrc_path)
def clang(csrc_path, with_comments=False):
cmd = ['clang', '-Xclang', '-ast-dump=json', "-c", csrc_path]
if with_comments:
cmd.append('-fparse-all-comments')
return subprocess.check_output(cmd)

def gen(header_path, source_path, module, main_prefix, dep_prefixes):
ast = clang(source_path)
def gen(header_path, source_path, module, main_prefix, dep_prefixes, with_comments=False):
ast = clang(source_path, with_comments=with_comments)
inp = json.loads(ast)
outp = {}
outp['module'] = module
outp['prefix'] = main_prefix
outp['dep_prefixes'] = dep_prefixes
outp['decls'] = []
for decl in inp['inner']:
is_dep = is_dep_decl(decl, dep_prefixes)
if is_api_decl(decl, main_prefix) or is_dep:
outp_decl = parse_decl(decl)
if outp_decl is not None:
outp_decl['is_dep'] = is_dep
outp_decl['dep_prefix'] = dep_prefix(decl, dep_prefixes)
outp['decls'].append(outp_decl)
with open(header_path, 'r') as f:
source = f.read()
for decl in inp['inner']:
is_dep = is_dep_decl(decl, dep_prefixes)
if is_api_decl(decl, main_prefix) or is_dep:
outp_decl = parse_decl(decl, source)
if outp_decl is not None:
outp_decl['is_dep'] = is_dep
outp_decl['dep_prefix'] = dep_prefix(decl, dep_prefixes)
outp['decls'].append(outp_decl)
with open(f'{module}.json', 'w') as f:
f.write(json.dumps(outp, indent=2));
return outp
25 changes: 24 additions & 1 deletion bindgen/gen_odin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
'sdtx_': 'debugtext',
'sshape_': 'shape',
'sglue_': 'glue',
'simgui_': 'imgui',
}

system_libs = {
Expand Down Expand Up @@ -76,6 +77,7 @@
'sdtx_': 'sokol_debugtext.c',
'sshape_': 'sokol_shape.c',
'sglue_': 'sokol_glue.c',
'simgui_': 'sokol_imgui.c',
}

ignores = [
Expand Down Expand Up @@ -420,6 +422,13 @@ def gen_c_imports(inp, c_prefix, prefix):
args = funcdecl_args_c(decl, prefix)
res_type = funcdecl_result_c(decl, prefix)
res_str = '' if res_type == '' else f'-> {res_type}'
if decl.get('comment'):
if decl.get('comment_multiline'):
l(" /*")
l(" " + " ".join(decl['comment'].splitlines(True)))
l(" */")
else:
l(" // " + decl['comment'].strip())
# Need to special case sapp_sg to avoid Odin's context keyword
if c_prefix == "sapp_sg":
l(f' @(link_name="{decl["name"]}")')
Expand All @@ -438,6 +447,13 @@ def gen_consts(decl, prefix):
def gen_struct(decl, prefix):
c_struct_name = check_override(decl['name'])
struct_name = as_struct_or_enum_type(c_struct_name, prefix)
if decl.get('comment'):
if decl.get('comment_multiline'):
l("/*")
l(decl["comment"])
l("*/")
else:
l("// " + decl['comment'].strip())
l(f'{struct_name} :: struct {{')
for field in decl['fields']:
field_name = check_override(field['name'])
Expand All @@ -452,6 +468,13 @@ def gen_struct(decl, prefix):

def gen_enum(decl, prefix):
enum_name = check_override(decl['name'])
if decl.get('comment'):
if decl.get('comment_multiline'):
l("/*")
l(decl["comment"])
l("*/")
else:
l("// " + decl['comment'].strip())
l(f'{as_struct_or_enum_type(enum_name, prefix)} :: enum i32 {{')
for item in decl['items']:
item_name = as_enum_item_name(check_override(item['name']))
Expand Down Expand Up @@ -529,7 +552,7 @@ def gen(c_header_path, c_prefix, dep_c_prefixes):
shutil.copyfile(c_header_path, f'{c_root}/{os.path.basename(c_header_path)}')
csource_path = get_csource_path(c_prefix)
module_name = module_names[c_prefix]
ir = gen_ir.gen(c_header_path, csource_path, module_name, c_prefix, dep_c_prefixes)
ir = gen_ir.gen(c_header_path, csource_path, module_name, c_prefix, dep_c_prefixes, with_comments=True)
gen_module(ir, c_prefix, dep_c_prefixes)
with open(f"{module_root}/{ir['module']}/{ir['module']}.odin", 'w', newline='\n') as f_outp:
f_outp.write(out_lines)

0 comments on commit 543bd50

Please sign in to comment.