Coverage for ci/cbindgen.py: 96%

231 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2026-05-07 14:30 +0000

1# mypy: ignore-errors 

2 

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. 

8 

9from pycparser import c_ast, parse_file 

10import sys 

11 

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 = {} 

26 

27 # Skip all function definitions, we don't bind those 

28 def visit_FuncDef(self, node): 

29 pass 

30 

31 def visit_Struct(self, node): 

32 if not node.name or not node.name.startswith('was'): 

33 return 

34 

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 

38 

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 

47 

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) 

56 

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" 

62 

63 for decl in node.decls: 

64 self.ret += " (\"{}\", {}),\n".format(decl.name, type_name(decl.type)) 

65 self.ret += " ]\n" 

66 

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)) 

70 

71 def visit_Union(self, node): 

72 if not node.name or not node.name.startswith('was'): 

73 return 

74 assert(node.decls) 

75 

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" 

87 

88 def visit_Enum(self, node): 

89 if not node.name or not node.name.startswith('was'): 

90 return 

91 

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) 

99 

100 

101 def visit_Typedef(self, node): 

102 if not node.name or not node.name.startswith('was'): 

103 return 

104 

105 if node.name in self.forward_declared and node.name == 'wasmtime_eqref': 

106 return 

107 

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] 

115 

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)) 

127 

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 

142 

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 

148 

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 

162 

163 ret = ty.type 

164 

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) 

179 

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) 

186 

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)) 

193 

194 

195def name(name): 

196 if name == 'global': 

197 return 'global_' 

198 return name 

199 

200 

201def type_name(ty, ptr=False, typing=False): 

202 while isinstance(ty, c_ast.TypeDecl): 

203 ty = ty.type 

204 

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)) 

212 

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) 

217 

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)) 

280 

281 

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 ) 

304 

305 v = Visitor() 

306 v.visit(ast) 

307 return v.ret 

308 

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")