Repository navigation
Expand file tree
/
Copy pathemit_embind.py
More file actions
584 lines (520 loc) · 26.6 KB
/
Copy pathemit_embind.py
File metadata and controls
584 lines (520 loc) · 26.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# SPDX-License-Identifier: BSD-3-Clause
"""IR -> Embind C++ emitter.
Produces a single mdx_bindings.cpp-shaped file from a BindModule. Layout
mirrors what we'd write by hand: helpers up top, EMSCRIPTEN_BINDINGS block
with shared math, constants, enums, tracks, classes, vectors, and
register_vector calls.
"""
from __future__ import annotations
import re
from io import StringIO
from .ir import (
BindClass, BindConstant, BindEnum, BindField, BindModule,
TypeKind, TypeRef,
)
from .parser import js_name_for_type, _short_name
# Whole-word-substitute bare whiteout primitives (`u32` etc.) with their
# fully-qualified spelling. Used wherever we splice libclang's raw param
# spellings into a context that doesn't `using namespace whiteout` —
# select_overload<> signatures live inside an EMSCRIPTEN_BINDINGS block
# which only opens up `emscripten`, so `u32` would be undeclared there.
_PRIMITIVE_RE = re.compile(
r'\b(u8|u16|u32|u64|i8|i16|i32|i64|f32|f64)\b')
def _qualify_primitives(raw: str) -> str:
return _PRIMITIVE_RE.sub(r'whiteout::\1', raw) if raw else raw
HEADER = '''// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
//
// AUTOGENERATED by tools/codegen — do not edit by hand.
// Regenerate via: python -m tools.codegen.codegen {module}
//
// Source headers and `@bind` annotations live under include/whiteout/.
// To change a binding, edit the C++ header (or the per-module config
// in tools/codegen/modules/) and re-run the generator.
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <array>
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
// Bridges std::optional<T> ⇄ JS `null | T` for any T Embind already knows.
// Must be included before the EMSCRIPTEN_BINDINGS block so the BindingType
// specialisation is in scope when the binding macros instantiate.
#include "optional_marshal.h"
'''
_HELPERS_BASE = '''
namespace {
template <typename T, std::size_t N>
std::vector<T> arrayToVec(const std::array<T, N>& a) {
return {a.begin(), a.end()};
}
template <typename T, std::size_t N>
void vecToArray(std::array<T, N>& a, const std::vector<T>& v) {
if (v.size() != N) {
throw std::runtime_error(
std::string("setter expected exactly ") + std::to_string(N)
+ " elements, got " + std::to_string(v.size()));
}
for (std::size_t i = 0; i < N; ++i) a[i] = v[i];
}
'''
_HELPERS_END = '''
} // namespace
'''
def _qualified(cls_or_enum_short: str, ns: str) -> str:
"""Sequence -> whiteout::mdx::Sequence (skip if already qualified)."""
if '::' in cls_or_enum_short and cls_or_enum_short.startswith(('whiteout::', 'std::')):
return cls_or_enum_short
# Top-level `whiteout` namespaces that show up in cross-module signatures
# (e.g. `interfaces::WorkerPool` in a textures/Mpq binding) need the
# `whiteout::` prefix, NOT the module-default `whiteout::<mod>` prefix.
if cls_or_enum_short.startswith(('interfaces::', 'utils::', 'common::',
'storages::', 'sno::', 'models::',
'wasm::', 'host::', 'textures::',
'mdx::', 'm2::', 'm3::', 'mpq::', 'casc::')):
return f'whiteout::{cls_or_enum_short}'
return f'{ns}::{cls_or_enum_short}'
_WHITEOUT_PRIMITIVES = {'u8', 'u16', 'u32', 'u64',
'i8', 'i16', 'i32', 'i64',
'f32', 'f64'}
# ── Buffer-friendly element descriptors ───────────────────────────────────
# Same shape as emit_pybind._buffer_descriptor: (scalar_cpp, components).
# A vector<Elem> with such an element is layout-compatible with a flat
# Scalar[N*components] array; we expose a zero-copy `.view()` returning the
# matching JS TypedArray (Uint8Array / Float32Array / etc.).
def _buffer_descriptor(t: TypeRef) -> tuple[str, int] | None:
if t.kind == TypeKind.PRIMITIVE:
short = _short_name(t.cpp_text)
if short in ('u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64',
'f32', 'f64'):
return f'whiteout::{short}', 1
if t.kind == TypeKind.NESTED:
short = _short_name(t.cpp_text).replace('::', '')
if short == 'Vector2f': return 'float', 2
if short == 'Vector3f': return 'float', 3
if short in ('Vector4f', 'Quaternion'): return 'float', 4
if short == 'ColorBGRA': return 'whiteout::u8', 4
return None
def _typed_array_for_scalar(scalar_cpp: str) -> str:
"""C++ scalar -> JS TypedArray subclass name (for TS declarations)."""
return {
'whiteout::u8': 'Uint8Array',
'whiteout::u16': 'Uint16Array',
'whiteout::u32': 'Uint32Array',
'whiteout::u64': 'BigUint64Array',
'whiteout::i8': 'Int8Array',
'whiteout::i16': 'Int16Array',
'whiteout::i32': 'Int32Array',
'whiteout::i64': 'BigInt64Array',
'whiteout::f32': 'Float32Array',
'whiteout::f64': 'Float64Array',
'float': 'Float32Array',
}[scalar_cpp]
def _cpp_type(t: TypeRef, ns: str) -> str:
"""Render a TypeRef back into C++ source text (fully qualified)."""
if t.kind == TypeKind.PRIMITIVE:
# whiteout::u32 etc. are aliases that aren't visible without the
# namespace qualifier from the EMSCRIPTEN_BINDINGS scope.
short = _short_name(t.cpp_text)
if short in _WHITEOUT_PRIMITIVES:
return f'whiteout::{short}'
return t.cpp_text
if t.kind == TypeKind.STRING:
return 'std::string'
if t.kind in (TypeKind.NESTED, TypeKind.ENUM):
# cpp_text is already canonicalised by classify_type — either a
# whiteout alias (for Vector3f etc.) or libclang's fully-qualified
# spelling. _qualified handles both the bare-name case and the
# cross-namespace case (`interfaces::X` → `whiteout::interfaces::X`).
return _qualified(t.cpp_text, ns)
if t.kind in (TypeKind.VECTOR, TypeKind.NESTED_VEC):
return f'std::vector<{_cpp_type(t.element, ns)}>'
if t.kind == TypeKind.ARRAY:
return f'std::array<{_cpp_type(t.element, ns)}, {t.array_size}>'
if t.kind == TypeKind.OPTIONAL:
return f'std::optional<{_cpp_type(t.element, ns)}>'
# UNKNOWN-kind fallthrough — qualify bare class names with the module
# namespace so they resolve at global scope in the binding cpp. Skip
# for `void` and other built-ins that look like identifiers.
if t.cpp_text in ('void', 'bool', 'char'):
return t.cpp_text
return _qualified(t.cpp_text, ns)
def _enum_qual(qual: str, ns: str) -> str:
"""Sequence::Flag -> whiteout::mdx::Sequence::Flag."""
return _qualified(qual, ns)
def _emit_enum(out: StringIO, e: BindEnum, ns: str):
# Prefer the enum's actual namespace (captured at parse time) over the
# module-default namespace; types in sub-namespaces (e.g. textures::blp)
# would otherwise be misqualified.
use_ns = e.cpp_namespace or ns
out.write(f' enum_<{_enum_qual(e.cpp_qualifier, use_ns)}>("{e.js_name}")\n')
for i, v in enumerate(e.values):
end = ';' if i == len(e.values) - 1 else ''
out.write(f' .value("{v.js_name}", {_enum_qual(v.cpp_qualifier, use_ns)}){end}\n')
out.write('\n')
def _emit_value_object(out: StringIO, c: BindClass, ns: str):
use_ns = c.cpp_namespace or ns
cpp_qual = (
f'whiteout::{c.cpp_qualifier}'
if c.cpp_qualifier in ('Vector2f', 'Vector3f', 'Vector4f', 'Quaternion')
else _qualified(c.cpp_qualifier, use_ns)
)
out.write(f' value_object<{cpp_qual}>("{c.js_name}")\n')
for f in c.fields:
out.write(f' .field("{f.name}", &{cpp_qual}::{f.cpp_name})\n')
out.write(' ;\n\n') # Defensive: trailing ; on its own line.
def _emit_method(out: StringIO, m, cls_qual: str, ns: str):
"""Emit one Embind .function(...) / .class_function(...) line.
Mirror of emit_pybind._emit_method:
- static methods → `.class_function(...)`
- bytes-in / bytes-out → lambda wrapper
- std::optional<T> return → plain return (the BindingType bridge in
optional_marshal.h does the marshalling)
- overloaded methods → `select_overload<Sig>(&Class::method)`
- trimmed default params → lambda wrapper (needs_wrapper)
"""
binder = 'class_function' if m.is_static else 'function'
# Helper: render the function-type signature for select_overload<...>.
# Embind's syntax embeds the `const` qualifier in the signature itself
# (`Ret(Args...) const`) rather than passing it as a runtime arg.
def _overload_sig() -> str:
ret_cpp = _cpp_type(m.return_type, ns) if m.return_type.cpp_text else 'void'
# For primitives, prefer the raw spelling (`size_t`, `u32`, ...) —
# `_cpp_type` canonicalises to fixed-width aliases which mismatch
# the method's declared signature on the wasm target (where
# `size_t` is 32-bit but `u64` is 64-bit). For class types, fall
# back to the fully-qualified canonical and reattach `&`/`*`/
# `const` from the raw spelling so they aren't dropped.
def _full(p):
raw = p.cpp_raw or ''
# _qualify_primitives returns `raw` unchanged when it contains
# no whiteout primitive — only trust it when it actually rewrote
# something (or the raw spelling is genuinely a known primitive
# like `size_t` that needs preserving over the canonical alias).
prim = _qualify_primitives(raw)
primitive_only = _PRIMITIVE_RE.search(raw) or any(
tok in raw for tok in ('size_t', 'bool', 'char ', 'float', 'double',
'short', 'int ', 'long', 'unsigned ', 'signed '))
if primitive_only and prim:
return prim
base = _cpp_type(p.type, ns)
if 'const ' in raw and not base.startswith('const '):
base = 'const ' + base
if '&' in raw:
base += ' &'
elif '*' in raw:
base += ' *'
return base
param_cpp = ', '.join(_full(p) for p in m.params)
sig = f'{ret_cpp}({param_cpp})'
if m.is_const and not m.is_static:
sig += ' const'
return sig
# Move-only class returns must go through a `val(std::move(...))`
# wrapper — Embind's default binding for the class type tries to copy
# via the value-type marshaller and won't compile against a deleted
# copy ctor.
# Reference returns (e.g. `Builder& declareX(...)`) of class types
# also need a wrapper: Embind's wire layer marshals the reference's
# pointee by value, which fails for move-only types and is wasteful
# for the common builder-chain idiom.
ret_is_class_ref = (
m.return_is_reference
and m.return_type.kind in (TypeKind.NESTED, TypeKind.UNKNOWN)
)
if ((m.return_is_move_only or ret_is_class_ref)
and m.return_type.kind != TypeKind.OPTIONAL
and not (m.bytes_in or m.bytes_out or m.needs_wrapper)):
m_needs_wrapper = True
else:
m_needs_wrapper = m.needs_wrapper
if not (m.bytes_in or m.bytes_out or m_needs_wrapper):
target = f'&{cls_qual}::{m.cpp_name}'
if m.is_overloaded:
target = f'select_overload<{_overload_sig()}>({target})'
# Raw-pointer params (typical for `WorkerPool*` & friends) require
# the `allow_raw_pointers()` policy or Embind refuses to bind.
has_raw_ptr_param = any(
'*' in (p.cpp_raw or '') for p in m.params
)
if has_raw_ptr_param:
out.write(f' .{binder}("{m.name}", {target}, allow_raw_pointers())\n')
else:
out.write(f' .{binder}("{m.name}", {target})\n')
return
# Lambda wrapper for marshalling spans / bytes / trimmed defaults.
# `std::span<const T>` (for any primitive T) goes through Embind's
# `convertJSArrayToNumberVector<T>` — accepts JS arrays and typed
# arrays alike. The intermediate vector copies into WASM heap (no
# true zero-copy across the JS/WASM boundary without sharing the
# underlying ArrayBuffer, which Embind doesn't do natively).
span_params = [(i, p) for i, p in enumerate(m.params)
if p.span_scalar is not None]
out.write(f' .{binder}("{m.name}",\n')
out.write(' optional_override([](')
if not m.is_static:
out.write(f'\n {cls_qual}& self')
sep = ',\n ' if not m.is_static else '\n '
for i, p in enumerate(m.params):
out.write(sep)
sep = ',\n '
if p.span_scalar is not None:
out.write(f'const emscripten::val& __js_arr_{i}')
else:
# Preserve `const`/`&`/`*` decorations from the cursor's original
# spelling so by-reference / by-pointer params (especially
# abstract-class interfaces like WorkerPool) don't degrade to
# by-value, and `const std::string&` doesn't lose its const.
base = _cpp_type(p.type, ns)
raw = p.cpp_raw or ''
if 'const ' in raw and not base.startswith('const '):
base = 'const ' + base
if '&' in raw and '&' not in base:
base += '&'
elif '*' in raw and '*' not in base:
base += '*'
out.write(f'{base} {p.name}')
out.write(') {\n')
for i, p in span_params:
short, _ = p.span_scalar
pname = p.name
out.write(f' auto __vec_{i} = emscripten::convertJSArrayToNumberVector<whiteout::{short}>(__js_arr_{i});\n')
out.write(f' std::span<const whiteout::{short}> {pname}(__vec_{i}.data(), __vec_{i}.size());\n')
qualified_call = (
f'{cls_qual}::{m.cpp_name}' if m.is_static else f'self.{m.cpp_name}'
)
args = ', '.join(p.name for p in m.params)
call = f'{qualified_call}({args})'
ret = m.return_type
if ret.cpp_text == 'void':
out.write(f' {call};\n')
out.write(' }))\n')
return
# Raw-pointer params (typical for abstract-base parameters like
# `WorkerPool*`) also require the policy on the wrapped binding.
needs_raw_ptr_policy = any('*' in (p.cpp_raw or '') for p in m.params)
if (ret.kind == TypeKind.OPTIONAL
and ret.element.kind in (TypeKind.NESTED, TypeKind.UNKNOWN)):
# `optional<class>` → heap pointer or nullptr → JS `null | T`.
# Routing through `to_optional_ptr<T>` (rather than `val(...)`)
# avoids Embind's wire-marshalling layer trying to copy-construct
# move-only classes like `mpq::Storage`. JS owns the resulting
# wrapper — caller must `.delete()` it.
elem_cpp = _cpp_type(ret.element, ns)
out.write(f' return whiteout::wasm::to_optional_ptr<{elem_cpp}>({call});\n')
needs_raw_ptr_policy = True
elif ret_is_class_ref:
# Builder-pattern reference return — fire the side effect and hand
# the JS-side wrapper back the *same* object pointer. Preserves
# method chaining (`builder.declareX(...).declareY(...).build()`)
# without forcing Embind to value-marshal a move-only class.
# The call expression is `self.method(...)` returning T& — wrap
# it in `&(...)` to turn that into a `T*`.
out.write(f' return &({call});\n')
needs_raw_ptr_policy = True
elif m.return_is_move_only:
# Plain move-only class return → heap pointer; JS owns it.
elem_cpp = _cpp_type(ret, ns)
out.write(f' return whiteout::wasm::to_heap_ptr<{elem_cpp}>({call});\n')
needs_raw_ptr_policy = True
else:
out.write(f' return {call};\n')
if needs_raw_ptr_policy:
# Embind requires the `allow_raw_pointers()` policy when a binding
# function returns `T*` directly — without it, the binding is
# rejected as unsafe.
out.write(' }), allow_raw_pointers())\n')
else:
out.write(' }))\n')
def _is_span_const_u8_type(t):
"""Lift the parser's predicate into the emitter."""
from .parser import _is_span_const_u8
return _is_span_const_u8(t)
def _emit_class(out: StringIO, c: BindClass, ns: str):
use_ns = c.cpp_namespace or ns
cpp_qual = _qualified(c.cpp_qualifier, use_ns)
# `class_<Derived, base<Base>>` is Embind's idiom for inheritance —
# base must be a template parameter, not a method on the class object.
if c.base_class:
out.write(f' class_<{cpp_qual}, base<{c.base_class}>>("{c.js_name}")\n')
else:
out.write(f' class_<{cpp_qual}>("{c.js_name}")\n')
# `optional<T>` returns need T's optional variant registered at
# runtime; emit it right after the class so the call is well-ordered
# against the class binding.
# (Done after the class body via the closing `;` — see end of fn.)
# Default constructor: skip when the C++ class has none (auto-detected
# at parse time) or when @bind no_default_ctor explicitly suppresses it
# (move-only factory types like mpq::Storage).
if not c.no_default_ctor:
out.write(' .constructor<>()\n')
# Helper: render ctor param types using the canonical type plus any
# `&` / `*` decorations from the cursor's original spelling.
def _ctor_param_type(p):
base = _cpp_type(p.type, ns)
raw = p.cpp_raw or ''
if '&' in raw:
base += '&'
elif '*' in raw:
base += '*'
return base
for ctor in c.constructors:
sig = ', '.join(_ctor_param_type(p) for p in ctor.params)
out.write(f' .constructor<{sig}>()\n')
# Skip array_with_view fields' .property — emit only the *View() function.
# Other fields use plain .property; std::array<T,N> goes through helpers.
array_helper_funcs = []
last_idx = len(c.fields) - 1
# For template instantiations (e.g. `AnimationTrack<Vector3f>`) the
# synthesised field list includes members inherited from a public base.
# `&Derived::base_field` has type `T Base::*`, which Embind's `.property`
# template can't bind on `class_<Derived>`. Down-cast the member pointer
# via static_cast: `T Base::*` → `T Derived::*` is well-defined for
# public single inheritance and is identity for own fields. Cheap, safe,
# and avoids reworking the parser's flatten step or every other backend.
is_template_instance = '<' in c.cpp_qualifier
for i, f in enumerate(c.fields):
end = ';' if i == last_idx and not array_helper_funcs else ''
if f.type.kind == TypeKind.ARRAY:
array_helper_funcs.append(f)
# plain property won't compile for std::array; skip here
if i == last_idx and not array_helper_funcs:
# nothing to emit; close trailing semi via next loop iteration.
pass
continue
# Vector<u8> with array_with_view: emit BOTH the property (read/write)
# AND a *View() function.
if is_template_instance:
field_t = _cpp_type(f.type, ns)
mptr = (f'static_cast<{field_t} {cpp_qual}::*>'
f'(&{cpp_qual}::{f.cpp_name})')
else:
mptr = f'&{cpp_qual}::{f.cpp_name}'
out.write(f' .property("{f.name}", {mptr})\n')
if f.array_with_view:
out.write(f' .function("{f.name}View",\n')
out.write(' optional_override([](const ' + cpp_qual + '& self) {\n')
out.write(f' return val(typed_memory_view(self.{f.cpp_name}.size(), self.{f.cpp_name}.data()));\n')
out.write(' }))\n')
# Now emit array helpers (getter + setter for each std::array field).
for i, f in enumerate(array_helper_funcs):
last = (i == len(array_helper_funcs) - 1)
end = ';' if last else ''
title = f.name[0].upper() + f.name[1:]
elem_t = _cpp_type(f.type.element, ns)
out.write(f' .function("get{title}",\n')
out.write(f' optional_override([](const {cpp_qual}& self) {{ return arrayToVec(self.{f.cpp_name}); }}))\n')
out.write(f' .function("set{title}",\n')
out.write(f' optional_override([]( {cpp_qual}& self, const std::vector<{elem_t}>& v) {{ vecToArray(self.{f.cpp_name}, v); }})){end}\n')
# Methods.
for m in c.methods:
_emit_method(out, m, cpp_qual, ns)
out.write(' ;\n\n')
def _vector_js_name(v: TypeRef, prefix: str) -> str:
# Vector container naming mirrors the hand-written file: Vector + JsName(T)
return 'Vector' + js_name_for_type(v.element, prefix)
def emit(module: BindModule) -> str:
ns = module.cpp_namespace
buf = StringIO()
buf.write(HEADER.format(module=module.name))
# User-supplied includes (so generated file knows the C++ types).
for h in module.headers:
buf.write(f'#include <{h.replace("include/", "")}>\n')
# Embind needs the full definition of every abstract base referenced
# by a binding signature (TypeID computation). The interfaces header
# defines WorkerPool / VirtualPathFileSystem / CascFileSystem /
# HttpHandler — almost every module touches one of these, so always
# include it.
if 'whiteout/interfaces.h' not in '\n'.join(module.headers):
buf.write('#include <whiteout/interfaces.h>\n')
buf.write('\n')
buf.write(_HELPERS_BASE)
buf.write(_HELPERS_END)
buf.write('\n')
buf.write(f'EMSCRIPTEN_BINDINGS({module.embind_block}) {{\n')
buf.write(' using namespace emscripten;\n\n')
# Constants.
if module.constants:
buf.write(' // ── Sentinel constants ───────────────────────────────────────────────\n')
for c in module.constants:
expr = c.cpp_expr if 'whiteout::' in c.cpp_expr else _qualified(c.cpp_expr, ns)
cast_t = f'whiteout::{c.cpp_type}' if c.cpp_type in _WHITEOUT_PRIMITIVES else c.cpp_type
buf.write(f' constant("{c.js_name}", static_cast<{cast_t}>({expr}));\n')
buf.write('\n')
skip = set(module.skip_class_js_names)
# Enums.
enums = [e for e in module.enums if e.js_name not in skip]
if enums:
buf.write(' // ── Enums ────────────────────────────────────────────────────────────\n')
for e in enums:
_emit_enum(buf, e, ns)
# Value-object math types first (other classes may reference them by value).
value_classes = [c for c in module.classes if c.is_value_object and c.js_name not in skip]
other_classes = [c for c in module.classes if not c.is_value_object and c.js_name not in skip]
if value_classes:
buf.write(' // ── Value-object types (plain JS objects) ────────────────────────────\n')
for c in value_classes:
_emit_value_object(buf, c, ns)
# Class types.
if other_classes:
buf.write(' // ── Classes ──────────────────────────────────────────────────────────\n')
for c in other_classes:
_emit_class(buf, c, ns)
# Runtime registration of `std::optional<T>` for every T that appears
# in an optional return. Without this, Embind throws "Cannot call X
# due to unbound types: std::optional<…>" at call time. The
# BindingType<std::optional<T>> bridge in optional_marshal.h handles
# the C++/JS conversion; this call wires it into Embind's registry.
optional_targets: list[str] = []
seen_opt: set[str] = set()
for c in module.classes:
for m in c.methods:
if m.return_type.kind != TypeKind.OPTIONAL:
continue
elem = m.return_type.element
elem_cpp = _cpp_type(elem, ns)
if elem_cpp in seen_opt:
continue
seen_opt.add(elem_cpp)
optional_targets.append(elem_cpp)
if optional_targets:
buf.write(' // ── Runtime registration for std::optional<T> returns ────────────────\n')
for t in optional_targets:
buf.write(f' register_optional<{t}>();\n')
buf.write('\n')
# Vector containers (must come AFTER class registration so types resolve).
if module.vector_types:
buf.write(' // ── Vector containers ────────────────────────────────────────────────\n')
seen = set(module.skip_vector_js_names)
for v in sorted(module.vector_types, key=lambda x: x.cpp_text):
js = _vector_js_name(v, module.js_prefix)
if js in seen:
continue
seen.add(js)
elem_cpp = _cpp_type(v.element, ns)
info = _buffer_descriptor(v.element)
if info is None:
buf.write(f' register_vector<{elem_cpp}>("{js}");\n')
else:
# Buffer-friendly: chain a `.view()` returning a zero-copy
# JS TypedArray aliased to the WASM heap. Vector* / Quaternion
# / ColorBGRA elements are reinterpret_cast'd to their flat
# scalar type so the JS view is a plain TypedArray of length
# `size * components` (callers stride manually, e.g.
# vec3.view()[i*3 + 0]).
scalar_cpp, components = info
buf.write(f' register_vector<{elem_cpp}>("{js}")\n')
buf.write(f' .function("view",\n')
buf.write(f' optional_override([](std::vector<{elem_cpp}>& self) {{\n')
if components == 1:
buf.write(f' return val(typed_memory_view(self.size(), self.data()));\n')
else:
buf.write(f' return val(typed_memory_view(\n')
buf.write(f' self.size() * {components},\n')
buf.write(f' reinterpret_cast<{scalar_cpp}*>(self.data())));\n')
buf.write(f' }}));\n')
buf.write('\n')
buf.write('}\n')
return buf.getvalue()