gen_d.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. #-------------------------------------------------------------------------------
  2. # Generate D bindings for Sokol library.
  3. #
  4. # D coding style:
  5. # - Types: PascalCase
  6. # - Functions: camelCase
  7. # - Variables: snake_case
  8. # - Doc-comments: /++ ... +/ for declarations, /// for fields, with proper wrapping
  9. #-------------------------------------------------------------------------------
  10. import gen_ir
  11. import os
  12. import shutil
  13. import sys
  14. import textwrap
  15. import logging
  16. from datetime import datetime
  17. import gen_util as util
  18. # Configure logging for debugging
  19. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  20. module_names = {
  21. 'slog_': 'log',
  22. 'sg_': 'gfx',
  23. 'sapp_': 'app',
  24. 'sargs_': 'args',
  25. 'stm_': 'time',
  26. 'saudio_': 'audio',
  27. 'sgl_': 'gl',
  28. 'sdtx_': 'debugtext',
  29. 'sshape_': 'shape',
  30. 'sglue_': 'glue',
  31. 'sfetch_': 'fetch',
  32. 'simgui_': 'imgui',
  33. 'snk_': 'nuklear',
  34. 'smemtrack_': 'memtrack',
  35. }
  36. c_source_paths = {
  37. 'slog_': 'sokol-d/src/sokol/c/sokol_log.c',
  38. 'sg_': 'sokol-d/src/sokol/c/sokol_gfx.c',
  39. 'sapp_': 'sokol-d/src/sokol/c/sokol_app.c',
  40. 'sargs_': 'sokol-d/src/sokol/c/sokol_args.c',
  41. 'stm_': 'sokol-d/src/sokol/c/sokol_time.c',
  42. 'saudio_': 'sokol-d/src/sokol/c/sokol_audio.c',
  43. 'sgl_': 'sokol-d/src/sokol/c/sokol_gl.c',
  44. 'sdtx_': 'sokol-d/src/sokol/c/sokol_debugtext.c',
  45. 'sshape_': 'sokol-d/src/sokol/c/sokol_shape.c',
  46. 'sglue_': 'sokol-d/src/sokol/c/sokol_glue.c',
  47. 'sfetch_': 'sokol-d/src/sokol/c/sokol_fetch.c',
  48. 'simgui_': 'sokol-d/src/sokol/c/sokol_imgui.c',
  49. 'snk_': 'sokol-d/src/sokol/c/sokol_nuklear.c',
  50. 'smemtrack_': 'sokol-d/src/sokol/c/sokol_memtrack.c',
  51. }
  52. ignores = [
  53. 'sdtx_printf',
  54. 'sdtx_vprintf',
  55. ]
  56. c_callbacks = [
  57. 'slog_func',
  58. 'nk_plugin_filter',
  59. ]
  60. overrides = {
  61. 'ref': '_ref',
  62. 'immutable': '_immutable',
  63. 'sgl_error': 'sgl_get_error',
  64. 'sgl_deg': 'sgl_as_degrees',
  65. 'sgl_rad': 'sgl_as_radians',
  66. 'sg_apply_uniforms.ub_slot': 'uint32_t',
  67. 'sg_draw.base_element': 'uint32_t',
  68. 'sg_draw.num_elements': 'uint32_t',
  69. 'sg_draw.num_instances': 'uint32_t',
  70. 'sg_dispatch.num_groups_x': 'uint32_t',
  71. 'sg_dispatch.num_groups_y': 'uint32_t',
  72. 'sg_dispatch.num_groups_z': 'uint32_t',
  73. 'sshape_element_range_t.base_element': 'uint32_t',
  74. 'sshape_element_range_t.num_elements': 'uint32_t',
  75. 'sdtx_font.font_index': 'uint32_t',
  76. 'SGL_NO_ERROR': 'SGL_ERROR_NO_ERROR',
  77. 'sfetch_continue': 'continue_fetching',
  78. 'struct nk_context': 'NkContext',
  79. 'nk_handle': 'NkHandle',
  80. 'nk_flags': 'NkFlags',
  81. }
  82. prim_types = {
  83. "int": "int",
  84. "bool": "bool",
  85. "char": "char",
  86. "int8_t": "byte",
  87. "uint8_t": "ubyte",
  88. "int16_t": "short",
  89. "uint16_t": "ushort",
  90. "int32_t": "int",
  91. "uint32_t": "uint",
  92. "int64_t": "long",
  93. "uint64_t": "ulong",
  94. "float": "float",
  95. "double": "double",
  96. "uintptr_t": "ulong",
  97. "intptr_t": "long",
  98. "size_t": "size_t",
  99. }
  100. prim_defaults = {
  101. 'int': '0',
  102. 'bool': 'false',
  103. 'int8_t': '0',
  104. 'uint8_t': '0',
  105. 'int16_t': '0',
  106. 'uint16_t': '0',
  107. 'int32_t': '0',
  108. 'uint32_t': '0',
  109. 'int64_t': '0',
  110. 'uint64_t': '0',
  111. 'float': '0.0f',
  112. 'double': '0.0',
  113. 'uintptr_t': '0',
  114. 'intptr_t': '0',
  115. 'size_t': '0'
  116. }
  117. class TypeConverter:
  118. def __init__(self, prefix, struct_types, enum_types):
  119. self.prefix = prefix
  120. self.struct_types = struct_types
  121. self.enum_types = enum_types
  122. def as_d_type(self, c_type, is_param=False, decl_name=None):
  123. c_type = c_type.strip()
  124. # Check for override first
  125. if c_type in overrides:
  126. return overrides[c_type]
  127. d_to_c_types = {v: k for k, v in prim_types.items()}
  128. if c_type in d_to_c_types:
  129. c_type = d_to_c_types[c_type]
  130. # Handle struct keyword in type (e.g., "struct nk_context *")
  131. if c_type.startswith('struct '):
  132. c_type = c_type[7:].strip() # Remove 'struct' prefix
  133. if c_type in overrides:
  134. return overrides[c_type]
  135. if c_type.endswith('*'):
  136. # External struct pointer, treat as opaque
  137. return 'void*' if is_param else 'void*'
  138. # External struct by value, treat as named type if overridden
  139. return overrides.get(c_type, c_type)
  140. if util.is_func_ptr(c_type):
  141. return self.as_func_ptr_type(c_type, decl_name)
  142. if c_type == "void":
  143. return "" if is_param else "void"
  144. if c_type in prim_types:
  145. return prim_types[c_type]
  146. if c_type in self.struct_types:
  147. return self.as_d_struct_type(c_type)
  148. if c_type in self.enum_types:
  149. return self.as_d_enum_type(c_type)
  150. if util.is_void_ptr(c_type):
  151. return "void*"
  152. if util.is_const_void_ptr(c_type):
  153. return "const(void)*"
  154. if util.is_string_ptr(c_type):
  155. return "const(char)*"
  156. if self.is_struct_ptr(c_type) or self.is_const_struct_ptr(c_type):
  157. struct_type = util.extract_ptr_type(c_type.strip())
  158. d_type = self.as_d_struct_type(struct_type)
  159. return f"const {d_type}*" if is_param else f"{d_type}*"
  160. if self.is_prim_ptr(c_type):
  161. prim_type = util.extract_ptr_type(c_type.strip())
  162. return f"{prim_types[prim_type]}*"
  163. if self.is_const_prim_ptr(c_type):
  164. prim_type = util.extract_ptr_type(c_type.strip())
  165. return f"const {prim_types[prim_type]}*"
  166. if util.is_array_type(c_type):
  167. return self.as_array_type(c_type)
  168. # Handle external types (e.g., nk_handle, nk_flags) not in struct_types or prim_types
  169. if c_type not in self.struct_types and c_type not in prim_types:
  170. if c_type.endswith('*'):
  171. # Treat pointer to unknown type as void*
  172. return 'void*' if is_param else 'void*'
  173. # Treat unknown type by value as itself (assuming it's defined elsewhere)
  174. return overrides.get(c_type, c_type)
  175. raise ValueError(f"Unsupported C type: {c_type} in declaration: {decl_name or 'unknown'}")
  176. def as_d_struct_type(self, s):
  177. parts = s.lower().split('_')
  178. outp = '' if s.startswith(self.prefix) else f'{parts[0]}.'
  179. return outp + ''.join(part.capitalize() for part in parts[1:] if part != 't')
  180. def as_d_enum_type(self, s):
  181. return self.as_d_struct_type(s)
  182. def parse_func_ptr_signature(self, c_type, decl, decl_name):
  183. if '(*)' in c_type:
  184. return_type = c_type[:c_type.index('(*)')].strip()
  185. params_str = c_type[c_type.index('(*)')+4:-1].strip()
  186. params = [p.strip() for p in params_str.split(',')] if params_str else []
  187. else:
  188. return_type = decl.get('return_type', 'void')
  189. params = [param['type'] for param in decl.get('params', [])]
  190. return return_type, params
  191. def as_func_ptr_type(self, c_type, decl_name, decl=None):
  192. return_type, params = self.parse_func_ptr_signature(c_type, decl or {}, decl_name)
  193. d_return_type = self.as_d_type(return_type, decl_name=decl_name)
  194. arg_types = []
  195. for param_type in params:
  196. if param_type and param_type != "void":
  197. try:
  198. arg_types.append(self.as_d_type(param_type, is_param=True, decl_name=decl_name))
  199. except ValueError as e:
  200. raise ValueError(f"Unsupported function pointer parameter type: {param_type} in {c_type} for declaration: {decl_name or 'unknown'}")
  201. args_str = ", ".join(arg_types) if arg_types else ""
  202. return f"extern(C) {d_return_type} function({args_str})"
  203. def as_array_type(self, c_type):
  204. array_type = util.extract_array_type(c_type)
  205. array_sizes = util.extract_array_sizes(c_type)
  206. base_type = self.as_d_type(array_type)
  207. dims = ''.join(f"[{size}]" for size in array_sizes)
  208. return f"{base_type}{dims}"
  209. def default_value(self, c_type):
  210. return prim_defaults.get(c_type, "")
  211. def is_struct_ptr(self, s):
  212. s = s.strip()
  213. return any(s == f"{struct_type} *" for struct_type in self.struct_types)
  214. def is_const_struct_ptr(self, s):
  215. s = s.strip()
  216. return any(s == f"const {struct_type} *" for struct_type in self.struct_types)
  217. def is_prim_ptr(self, s):
  218. s = s.strip()
  219. return any(s == f"{prim_type} *" for prim_type in prim_types)
  220. def is_const_prim_ptr(self, s):
  221. s = s.strip()
  222. return any(s == f"const {prim_type} *" for prim_type in prim_types)
  223. # Global state
  224. struct_types = []
  225. enum_types = []
  226. enum_items = {}
  227. out_lines = ''
  228. def reset_globals():
  229. global struct_types, enum_types, enum_items, out_lines
  230. struct_types = []
  231. enum_types = []
  232. enum_items = {}
  233. out_lines = ''
  234. def l(s):
  235. global out_lines
  236. out_lines += s + '\n'
  237. def format_comment(comment, indent="", multiline=False):
  238. if not comment:
  239. return
  240. comment = comment.strip()
  241. # Escape nested comment delimiters to ensure valid D code
  242. comment = comment.replace('/++', '/+ /').replace('+/', '/ +/')
  243. if multiline:
  244. # Split by newlines to preserve empty lines
  245. lines = [line.rstrip() for line in comment.split('\n')]
  246. l(f"{indent}/++")
  247. for line in lines:
  248. l(f"{indent}+ {line}")
  249. l(f"{indent}+/")
  250. else:
  251. for line in comment.split('\n'):
  252. l(f"{indent}/// {line.strip()}")
  253. def as_enum_item_name(s):
  254. outp = s.lstrip('_').split('_', 2)[-1].capitalize()
  255. return '_' + outp if outp[0].isdigit() else outp
  256. def pre_parse(inp):
  257. global struct_types, enum_types, enum_items
  258. for decl in inp['decls']:
  259. if decl['kind'] == 'struct':
  260. struct_types.append(decl['name'])
  261. elif decl['kind'] == 'enum':
  262. enum_name = decl['name']
  263. enum_types.append(enum_name)
  264. enum_items[enum_name] = [as_enum_item_name(item['name']) for item in decl['items']]
  265. def gen_nuklear_types():
  266. """Generate type declarations for Nuklear external types."""
  267. l("/++ Nuklear external type declarations +/")
  268. l("extern(C) struct NkContext;")
  269. l("extern(C) union NkHandle {")
  270. l(" void* ptr;")
  271. l(" int id;")
  272. l("}")
  273. l("alias NkFlags = uint;")
  274. l("alias nk_plugin_filter = extern(C) int function(const(NkContext)*, NkHandle, int*, int) @system @nogc nothrow;")
  275. def gen_struct(decl, type_converter):
  276. struct_name = overrides.get(decl['name'], decl['name'])
  277. d_type = type_converter.as_d_struct_type(struct_name)
  278. format_comment(decl.get('comment', ''), multiline=True)
  279. l(f"extern(C) struct {d_type} {{")
  280. used_field_names = set()
  281. for field in decl['fields']:
  282. field_key = f"{struct_name}.{field['name']}"
  283. field_name = overrides.get(field['name'], field['name'])
  284. field_type = overrides.get(field_key, field['type'])
  285. if field_name in used_field_names or field_name in prim_types.values():
  286. field_name = f"{field_name}_field"
  287. used_field_names.add(field_name)
  288. d_type_str = type_converter.as_d_type(field_type, decl_name=field_key)
  289. default = type_converter.default_value(field_type)
  290. if default:
  291. default_value = f" = {default}"
  292. elif util.is_func_ptr(field_type):
  293. default_value = " = null"
  294. elif type_converter.is_struct_ptr(field_type) or type_converter.is_const_struct_ptr(field_type):
  295. default_value = " = null"
  296. elif util.is_void_ptr(field_type) or util.is_const_void_ptr(field_type) or util.is_string_ptr(field_type):
  297. default_value = " = null"
  298. elif field_type in type_converter.struct_types:
  299. default_value = " = {}"
  300. elif field_type in type_converter.enum_types:
  301. enum_name = field_type
  302. if enum_name in enum_items and enum_items[enum_name]:
  303. default_value = f" = {type_converter.as_d_enum_type(enum_name)}.{enum_items[enum_name][0]}"
  304. else:
  305. default_value = " = 0"
  306. elif util.is_array_type(field_type):
  307. array_type = util.extract_array_type(field_type)
  308. array_sizes = util.extract_array_sizes(field_type)
  309. if array_type in prim_types and array_sizes:
  310. # Handle all primitive arrays with proper defaults
  311. default_value = f" = [{', '.join([prim_defaults[array_type]] * int(array_sizes[0]))}]"
  312. elif array_type in type_converter.struct_types or array_type in type_converter.enum_types:
  313. default_value = " = []"
  314. else:
  315. default_value = " = null"
  316. else:
  317. default_value = ""
  318. format_comment(field.get('comment', ''), " ")
  319. l(f" {d_type_str} {field_name}{default_value};")
  320. l("}")
  321. def gen_consts(decl, prefix):
  322. format_comment(decl.get('comment', ''), multiline=True)
  323. for item in decl['items']:
  324. item_name = overrides.get(item['name'], item['name'])
  325. format_comment(item.get('comment', ''))
  326. l(f"enum {util.as_lower_snake_case(item_name, prefix)} = {item['value']};")
  327. def gen_enum(decl, type_converter):
  328. enum_name = overrides.get(decl['name'], decl['name'])
  329. format_comment(decl.get('comment', ''), multiline=True)
  330. l(f"enum {type_converter.as_d_enum_type(enum_name)} {{")
  331. for item in decl['items']:
  332. item_name = as_enum_item_name(overrides.get(item['name'], item['name']))
  333. if item_name != "Force_u32":
  334. format_comment(item.get('comment', ''))
  335. l(f" {item_name}{f' = {item['value']}' if 'value' in item else ''},")
  336. l("}")
  337. def gen_func(decl, type_converter, prefix):
  338. c_func_name = decl['name']
  339. d_func_name = util.as_lower_camel_case(overrides.get(c_func_name, c_func_name), prefix)
  340. format_comment(decl.get('comment', ''), multiline=True)
  341. if c_func_name == 'slog_func':
  342. params = []
  343. for param in decl['params']:
  344. param_name = param['name']
  345. param_key = f"{c_func_name}.{param_name}"
  346. param_type = overrides.get(param_key, param['type'])
  347. param_d_type = type_converter.as_d_type(param_type, is_param=True, decl_name=param_key)
  348. params.append(f"{param_d_type} {param_name}")
  349. params_str = ", ".join(params) if params else ""
  350. result_type = type_converter.as_d_type(decl['type'][:decl['type'].index('(')].strip(), decl_name=c_func_name)
  351. l(f"extern(C) {result_type} {c_func_name}({params_str}) @system @nogc nothrow pure;")
  352. format_comment(decl.get('comment', ''), multiline=True)
  353. l(f"alias func = {c_func_name};")
  354. return
  355. if c_func_name in c_callbacks:
  356. l(f"alias {d_func_name} = {type_converter.as_func_ptr_type(decl['type'], c_func_name, decl)};")
  357. return
  358. params = []
  359. wrapper_params = []
  360. call_args = []
  361. for param in decl['params']:
  362. param_name = param['name']
  363. param_key = f"{c_func_name}.{param_name}"
  364. param_type = overrides.get(param_key, param['type'])
  365. param_d_type = type_converter.as_d_type(param_type, is_param=True, decl_name=param_key)
  366. wrapper_d_type = param_d_type
  367. if type_converter.is_struct_ptr(param_type) or type_converter.is_const_struct_ptr(param_type):
  368. wrapper_d_type = f"scope ref {type_converter.as_d_struct_type(util.extract_ptr_type(param_type.strip()))}"
  369. params.append(f"{param_d_type} {param_name}")
  370. wrapper_params.append(f"{wrapper_d_type} {param_name}")
  371. if 'scope ref' in wrapper_d_type:
  372. call_args.append(f"&{param_name}")
  373. else:
  374. call_args.append(param_name)
  375. result_type = type_converter.as_d_type(decl['type'][:decl['type'].index('(')].strip(), decl_name=c_func_name)
  376. params_str = ", ".join(params) if params else ""
  377. wrapper_params_str = ", ".join(wrapper_params) if wrapper_params else ""
  378. call_args_str = ", ".join(call_args) if call_args else ""
  379. l(f"extern(C) {result_type} {c_func_name}({params_str}) @system @nogc nothrow pure;")
  380. l(f"{result_type} {d_func_name}({wrapper_params_str}) @trusted @nogc nothrow pure {{")
  381. l(f" {'return ' if result_type != 'void' else ''}{c_func_name}({call_args_str});")
  382. l("}")
  383. def gen_imports(inp, dep_prefixes):
  384. for dep_prefix in dep_prefixes:
  385. if dep_prefix in module_names:
  386. l(f'import {dep_prefix[:-1]} = sokol.{module_names[dep_prefix]};')
  387. l('')
  388. def gen_module(inp, dep_prefixes, c_header_path):
  389. reset_globals()
  390. header_comment = f"""
  391. Machine generated D bindings for Sokol library.
  392. Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  393. Source header: {os.path.basename(c_header_path)}
  394. Module: sokol.{inp['module']}
  395. Do not edit manually; regenerate using gen_d.py.
  396. """
  397. format_comment(header_comment, multiline=True)
  398. l(f'module sokol.{inp["module"]};')
  399. logging.info(f"Generating imports for module {inp['module']}")
  400. gen_imports(inp, dep_prefixes)
  401. # Add Nuklear types for the nuklear module
  402. if inp['module'] == 'nuklear':
  403. gen_nuklear_types()
  404. pre_parse(inp)
  405. type_converter = TypeConverter(inp['prefix'], struct_types, enum_types)
  406. for decl in inp['decls']:
  407. if not decl.get('is_dep', False) and (decl.get('kind', '') != 'func' or decl['name'] not in ignores):
  408. if decl['kind'] == 'consts':
  409. gen_consts(decl, inp['prefix'])
  410. elif decl['kind'] == 'struct':
  411. gen_struct(decl, type_converter)
  412. elif decl['kind'] == 'enum':
  413. gen_enum(decl, type_converter)
  414. elif decl['kind'] == 'func':
  415. gen_func(decl, type_converter, inp['prefix'])
  416. def prepare():
  417. logging.info("Preparing directories for D bindings generation")
  418. print('=== Generating D bindings:')
  419. os.makedirs('sokol-d/src/sokol/c', exist_ok=True)
  420. def gen(c_header_path, c_prefix, dep_c_prefixes):
  421. if not os.path.isfile(c_header_path):
  422. raise FileNotFoundError(f"Header file not found: {c_header_path}")
  423. if c_prefix not in module_names:
  424. logging.warning(f"Skipping generation for prefix {c_prefix}")
  425. print(f' >> warning: skipping generation for {c_prefix} prefix...')
  426. return
  427. module_name = module_names[c_prefix]
  428. c_source_path = c_source_paths[c_prefix]
  429. logging.info(f"Generating bindings for {c_header_path} => {module_name}")
  430. print(f' {c_header_path} => {module_name}')
  431. reset_globals()
  432. shutil.copyfile(c_header_path, f'sokol-d/src/sokol/c/{os.path.basename(c_header_path)}')
  433. ir = gen_ir.gen(c_header_path, c_source_path, module_name, c_prefix, dep_c_prefixes, with_comments=True)
  434. gen_module(ir, dep_c_prefixes, c_header_path)
  435. output_path = f"sokol-d/src/sokol/{ir['module']}.d"
  436. with open(output_path, 'w', newline='\n') as f_outp:
  437. f_outp.write(out_lines)