Coverage for ci/cbindgen.py: 96%
231 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-05-07 14:30 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2026-05-07 14:30 +0000
1# mypy: ignore-errors
3# This is a small script to parse the header files from wasmtime and generate
4# appropriate function definitions in Python for each exported function. This
5# also reflects types into Python with `ctypes`. While there's at least one
6# other generate that does this already it seemed to not quite fit our purposes
7# with lots of extra an unnecessary boilerplate.
9from pycparser import c_ast, parse_file
10import sys
12class Visitor(c_ast.NodeVisitor):
13 def __init__(self):
14 self.ret = ''
15 self.ret += '# flake8: noqa\n'
16 self.ret += '#\n'
17 self.ret += '# This is a procedurally generated file, DO NOT EDIT\n'
18 self.ret += '# instead edit `./ci/cbindgen.py` at the root of the repo\n'
19 self.ret += '\n'
20 self.ret += 'import ctypes\n'
21 self.ret += 'from typing import Any\n'
22 self.ret += 'from enum import Enum, auto\n'
23 self.ret += 'from ._ffi import dll, wasm_val_t, wasm_ref_t\n'
24 self.typedefs = {}
25 self.forward_declared = {}
27 # Skip all function definitions, we don't bind those
28 def visit_FuncDef(self, node):
29 pass
31 def visit_Struct(self, node):
32 if not node.name or not node.name.startswith('was'):
33 return
35 # This is hand-generated since it has an anonymous union in it
36 if node.name == 'wasm_val_t' or node.name == 'wasm_ref_t':
37 return
39 self.ret += "\n"
40 if not node.decls:
41 if node.name in self.forward_declared:
42 return
43 self.forward_declared[node.name] = True
44 self.ret += "class {}(ctypes.Structure):\n".format(node.name)
45 self.ret += " pass\n"
46 return
48 anon_decl = 0
49 for decl in node.decls:
50 if not decl.name:
51 assert(isinstance(decl.type, c_ast.Struct))
52 decl.type.name = node.name + '_anon_' + str(anon_decl)
53 self.visit_Struct(decl.type)
54 anon_decl += 1
55 decl.name = '_anon_' + str(anon_decl)
57 if node.name in self.forward_declared:
58 self.ret += "{}._fields_ = [\n".format(node.name)
59 else:
60 self.ret += "class {}(ctypes.Structure):\n".format(node.name)
61 self.ret += " _fields_ = [\n"
63 for decl in node.decls:
64 self.ret += " (\"{}\", {}),\n".format(decl.name, type_name(decl.type))
65 self.ret += " ]\n"
67 if not node.name in self.forward_declared:
68 for decl in node.decls:
69 self.ret += " {}: {}\n".format(decl.name, type_name(decl.type, typing=True))
71 def visit_Union(self, node):
72 if not node.name or not node.name.startswith('was'):
73 return
74 assert(node.decls)
76 self.ret += "\n"
77 self.ret += "class {}(ctypes.Union):\n".format(node.name)
78 self.ret += " _fields_ = [\n"
79 for decl in node.decls:
80 self.ret += " (\"{}\", {}),\n".format(name(decl.name), type_name(decl.type))
81 self.ret += " ]\n"
82 for decl in node.decls:
83 self.ret += " {}: {}".format(name(decl.name), type_name(decl.type, typing=True))
84 if decl.name == 'v128':
85 self.ret += ' # type: ignore'
86 self.ret += "\n"
88 def visit_Enum(self, node):
89 if not node.name or not node.name.startswith('was'):
90 return
92 self.ret += "\n"
93 self.ret += "class {}(Enum):\n".format(node.name)
94 for enumerator in node.values.enumerators:
95 if enumerator.value:
96 self.ret += " {} = {}\n".format(enumerator.name, enumerator.value.value)
97 else:
98 self.ret += " {} = auto()\n".format(enumerator.name)
101 def visit_Typedef(self, node):
102 if not node.name or not node.name.startswith('was'):
103 return
105 if node.name in self.forward_declared and node.name == 'wasmtime_eqref':
106 return
108 # Given anonymous structs in typedefs names by default.
109 if isinstance(node.type, c_ast.TypeDecl):
110 if isinstance(node.type.type, c_ast.Struct) or \
111 isinstance(node.type.type, c_ast.Union):
112 if node.type.type.name is None:
113 if node.name.endswith('_t'):
114 node.type.type.name = node.name[:-2]
116 self.visit(node.type)
117 tyname = type_name(node.type)
118 if tyname != node.name:
119 self.ret += "\n"
120 if isinstance(node.type, c_ast.ArrayDecl):
121 self.ret += "{} = {} * {}\n".format(node.name, type_name(node.type.type), node.type.dim.value)
122 else:
123 if node.name in self.typedefs:
124 return
125 self.typedefs[node.name] = type_name(node.type)
126 self.ret += "{} = {}\n".format(node.name, type_name(node.type))
128 def visit_FuncDecl(self, node):
129 if isinstance(node.type, c_ast.TypeDecl):
130 ptr = False
131 ty = node.type
132 elif isinstance(node.type, c_ast.PtrDecl):
133 ptr = True
134 ty = node.type.type
135 name = ty.declname
136 # This is probably a type, skip it
137 if name.endswith('_t'):
138 return
139 # Skip anything not related to wasi or wasm
140 if not name.startswith('was'):
141 return
143 # This function forward-declares `wasm_instance_t` which doesn't work
144 # with this binding generator, but for now this isn't used anyway so
145 # just skip it.
146 if name == 'wasm_frame_instance':
147 return
149 # FIXME(bytecodealliance/wasmtime#13149)
150 if name == 'wasm_tagtype_vec_new_empty':
151 return
152 if name == 'wasm_tagtype_vec_new_uninitialized':
153 return
154 if name == 'wasm_tagtype_vec_new':
155 return
156 if name == 'wasm_tagtype_vec_copy':
157 return
158 if name == 'wasm_tagtype_vec_delete':
159 return
160 if name == 'wasmtime_anyref_to_eqref':
161 return
163 ret = ty.type
165 argpairs = []
166 argtypes = []
167 argnames = []
168 if node.args:
169 for i, param in enumerate(node.args.params):
170 argname = param.name
171 if not argname or argname == "import" or argname == "global":
172 argname = "arg{}".format(i)
173 tyname = type_name(param.type)
174 if i == 0 and tyname == "None":
175 continue
176 argpairs.append("{}: Any".format(argname))
177 argnames.append(argname)
178 argtypes.append(tyname)
180 # It seems like this is the actual return value of the function, not a
181 # pointer. Special-case this so the type-checking agrees with runtime.
182 if type_name(ret, ptr) == 'c_void_p':
183 retty = 'int'
184 else:
185 retty = type_name(node.type, ptr, typing=True)
187 self.ret += "\n"
188 self.ret += "_{0} = dll.{0}\n".format(name)
189 self.ret += "_{}.restype = {}\n".format(name, type_name(ret, ptr))
190 self.ret += "_{}.argtypes = [{}]\n".format(name, ', '.join(argtypes))
191 self.ret += "def {}({}) -> {}:\n".format(name, ', '.join(argpairs), retty)
192 self.ret += " return _{}({}) # type: ignore\n".format(name, ', '.join(argnames))
195def name(name):
196 if name == 'global':
197 return 'global_'
198 return name
201def type_name(ty, ptr=False, typing=False):
202 while isinstance(ty, c_ast.TypeDecl):
203 ty = ty.type
205 if ptr:
206 if typing:
207 return "ctypes._Pointer"
208 if isinstance(ty, c_ast.IdentifierType) and ty.names[0] == "void":
209 return "ctypes.c_void_p"
210 elif not isinstance(ty, c_ast.FuncDecl):
211 return "ctypes.POINTER({})".format(type_name(ty, False, typing))
213 if isinstance(ty, c_ast.IdentifierType):
214 if ty.names == ['unsigned', 'char']:
215 return "int" if typing else "ctypes.c_ubyte"
216 assert(len(ty.names) == 1)
218 if ty.names[0] == "void":
219 return "None"
220 elif ty.names[0] == "_Bool":
221 return "bool" if typing else "ctypes.c_bool"
222 elif ty.names[0] == "byte_t":
223 return "ctypes.c_ubyte"
224 elif ty.names[0] == "int8_t":
225 return "ctypes.c_int8"
226 elif ty.names[0] == "uint8_t":
227 return "ctypes.c_uint8"
228 elif ty.names[0] == "int16_t":
229 return "ctypes.c_int16"
230 elif ty.names[0] == "uint16_t":
231 return "ctypes.c_uint16"
232 elif ty.names[0] == "int32_t":
233 return "int" if typing else "ctypes.c_int32"
234 elif ty.names[0] == "uint32_t":
235 return "int" if typing else "ctypes.c_uint32"
236 elif ty.names[0] == "uint64_t":
237 return "int" if typing else "ctypes.c_uint64"
238 elif ty.names[0] == "int64_t":
239 return "int" if typing else "ctypes.c_int64"
240 elif ty.names[0] == "float32_t":
241 return "float" if typing else "ctypes.c_float"
242 elif ty.names[0] == "float64_t":
243 return "float" if typing else "ctypes.c_double"
244 elif ty.names[0] == "size_t":
245 return "int" if typing else "ctypes.c_size_t"
246 elif ty.names[0] == "ptrdiff_t":
247 return "int" if typing else "ctypes.c_ssize_t"
248 elif ty.names[0] == "char":
249 return "ctypes.c_char"
250 elif ty.names[0] == "int":
251 return "int" if typing else "ctypes.c_int"
252 # ctypes values can't stand as typedefs, so just use the pointer type here
253 elif typing and 'callback_t' in ty.names[0]:
254 return "ctypes._Pointer"
255 elif typing and ('size' in ty.names[0] or 'pages' in ty.names[0]):
256 return "int"
257 return ty.names[0]
258 elif isinstance(ty, c_ast.Struct):
259 return ty.name
260 elif isinstance(ty, c_ast.Union):
261 return ty.name
262 elif isinstance(ty, c_ast.Enum):
263 return ty.name
264 elif isinstance(ty, c_ast.FuncDecl):
265 tys = []
266 # TODO: apparently errors are thrown if we faithfully represent the
267 # pointer type here, seems odd?
268 if isinstance(ty.type, c_ast.PtrDecl):
269 tys.append("ctypes.c_size_t")
270 else:
271 tys.append(type_name(ty.type))
272 if ty.args and ty.args.params:
273 for param in ty.args.params:
274 tys.append(type_name(param.type))
275 return "ctypes.CFUNCTYPE({})".format(', '.join(tys))
276 elif isinstance(ty, c_ast.PtrDecl) or isinstance(ty, c_ast.ArrayDecl):
277 return type_name(ty.type, True, typing)
278 else:
279 raise RuntimeError("unknown {}".format(ty))
282def run():
283 ast = parse_file(
284 './wasmtime/include/wasmtime.h',
285 use_cpp=True,
286 cpp_path='gcc',
287 cpp_args=[
288 '-E',
289 '-I./wasmtime/include',
290 '-D__attribute__(x)=',
291 '-D__asm__(x)=',
292 '-D__asm(x)=',
293 '-D__volatile__(x)=',
294 '-D_Static_assert(x, y)=',
295 '-Dstatic_assert(x, y)=',
296 '-D__restrict=',
297 '-D__restrict__=',
298 '-D__extension__=',
299 '-D__inline__=',
300 '-D__signed=',
301 '-D__builtin_va_list=int',
302 ]
303 )
305 v = Visitor()
306 v.visit(ast)
307 return v.ret
309if __name__ == "__main__":
310 with open("wasmtime/_bindings.py", "w") as f:
311 f.write(run())
312elif sys.platform == 'linux':
313 with open("wasmtime/_bindings.py", "r") as f:
314 contents = f.read()
315 if contents != run():
316 raise RuntimeError("bindings need an update, run this script")