diff --git a/emcc.py b/emcc.py index 8870027348fcf..df260fd02b3d5 100644 --- a/emcc.py +++ b/emcc.py @@ -140,6 +140,7 @@ def __init__(self): self.ignore_dynamic_linking = False self.shell_path = None self.source_map_base = '' + self.emit_tsd = '' self.embind_emit_tsd = '' self.emrun = False self.cpu_profiler = False @@ -1276,6 +1277,9 @@ def consume_arg_file(): options.source_map_base = consume_arg() elif check_arg('--embind-emit-tsd'): options.embind_emit_tsd = consume_arg() + elif check_arg('--emit-tsd'): + diagnostics.warning('experimental', '--emit-tsd is still experimental. Not all definitions are generated.') + options.emit_tsd = consume_arg() elif check_flag('--no-entry'): options.no_entry = True elif check_arg('--js-library'): diff --git a/src/embind/embind_gen.js b/src/embind/embind_gen.js index 98735cec8fa40..3d1c962838e38 100644 --- a/src/embind/embind_gen.js +++ b/src/embind/embind_gen.js @@ -365,7 +365,7 @@ var LibraryEmbind = { def.print(this.typeToJsName.bind(this), out); } // Print module definitions - out.push('export interface MainModule {\n'); + out.push('interface EmbindModule {\n'); for (const def of this.definitions) { if (!def.printModuleEntry) { continue; diff --git a/test/other/embind_tsgen.d.ts b/test/other/embind_tsgen.d.ts index 56abfe50cac5c..37e2898f98ea7 100644 --- a/test/other/embind_tsgen.d.ts +++ b/test/other/embind_tsgen.d.ts @@ -1,3 +1,8 @@ +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { + _main(_0: number, _1: number): number; +} + export interface Test { x: number; readonly y: number; @@ -69,7 +74,7 @@ export interface DerivedClass extends BaseClass { export type ValArr = [ number, number, number ]; -export interface MainModule { +interface EmbindModule { Test: {staticFunction(_0: number): number; staticFunctionWithParam(x: number): number; staticProperty: number}; class_returning_fn(): Test; class_unique_ptr_returning_fn(): Test; @@ -95,3 +100,4 @@ export interface MainModule { string_test(_0: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): string; wstring_test(_0: string): string; } +export type MainModule = WasmModule & EmbindModule; diff --git a/test/other/embind_tsgen_bigint.d.ts b/test/other/embind_tsgen_bigint.d.ts index 16219f6d98b92..0bb48e2908f01 100644 --- a/test/other/embind_tsgen_bigint.d.ts +++ b/test/other/embind_tsgen_bigint.d.ts @@ -1,3 +1,8 @@ -export interface MainModule { +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { +} + +interface EmbindModule { bigintFn(_0: bigint): bigint; } +export type MainModule = WasmModule & EmbindModule; diff --git a/test/other/embind_tsgen_ignore_1.d.ts b/test/other/embind_tsgen_ignore_1.d.ts new file mode 100644 index 0000000000000..4a93af9ca06a6 --- /dev/null +++ b/test/other/embind_tsgen_ignore_1.d.ts @@ -0,0 +1,110 @@ +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { + _pthread_self(): number; + _main(_0: number, _1: number): number; + __emscripten_tls_init(): number; + __emscripten_proxy_main(_0: number, _1: number): number; + __embind_initialize_bindings(): void; + __emscripten_thread_init(_0: number, _1: number, _2: number, _3: number, _4: number, _5: number): void; + __emscripten_thread_crashed(): void; + __emscripten_thread_exit(_0: number): void; +} + +export interface Test { + x: number; + readonly y: number; + functionOne(_0: number, _1: number): number; + functionTwo(_0: number, _1: number): number; + functionFour(_0: boolean): number; + functionFive(x: number, y: number): number; + constFn(): number; + longFn(_0: number): number; + functionThree(_0: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): number; + functionSix(str: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): number; + delete(): void; +} + +export interface BarValue { + value: T; +} +export type Bar = BarValue<0>|BarValue<1>|BarValue<2>; + +export interface EmptyEnumValue { + value: T; +} +export type EmptyEnum = never/* Empty Enumerator */; + +export type ValArrIx = [ Bar, Bar, Bar, Bar ]; + +export interface IntVec { + push_back(_0: number): void; + resize(_0: number, _1: number): void; + size(): number; + set(_0: number, _1: number): boolean; + get(_0: number): any; + delete(): void; +} + +export interface Foo { + process(_0: Test): void; + delete(): void; +} + +export type ValObj = { + foo: Foo, + bar: Bar +}; + +export interface ClassWithConstructor { + fn(_0: number): number; + delete(): void; +} + +export interface ClassWithTwoConstructors { + delete(): void; +} + +export interface ClassWithSmartPtrConstructor { + fn(_0: number): number; + delete(): void; +} + +export interface BaseClass { + fn(_0: number): number; + delete(): void; +} + +export interface DerivedClass extends BaseClass { + fn2(_0: number): number; + delete(): void; +} + +export type ValArr = [ number, number, number ]; + +interface EmbindModule { + Test: {staticFunction(_0: number): number; staticFunctionWithParam(x: number): number; staticProperty: number}; + class_returning_fn(): Test; + class_unique_ptr_returning_fn(): Test; + a_class_instance: Test; + an_enum: Bar; + Bar: {valueOne: BarValue<0>, valueTwo: BarValue<1>, valueThree: BarValue<2>}; + EmptyEnum: {}; + enum_returning_fn(): Bar; + IntVec: {new(): IntVec}; + Foo: {}; + ClassWithConstructor: {new(_0: number, _1: ValArr): ClassWithConstructor}; + ClassWithTwoConstructors: {new(): ClassWithTwoConstructors; new(_0: number): ClassWithTwoConstructors}; + ClassWithSmartPtrConstructor: {new(_0: number, _1: ValArr): ClassWithSmartPtrConstructor}; + BaseClass: {}; + DerivedClass: {}; + a_bool: boolean; + an_int: number; + global_fn(_0: number, _1: number): number; + optional_test(_0: Foo | undefined): number | undefined; + smart_ptr_function(_0: ClassWithSmartPtrConstructor): number; + smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor): number; + function_with_callback_param(_0: (message: string) => void): number; + string_test(_0: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): string; + wstring_test(_0: string): string; +} +export type MainModule = WasmModule & EmbindModule; diff --git a/test/other/embind_tsgen_ignore_2.d.ts b/test/other/embind_tsgen_ignore_2.d.ts new file mode 100644 index 0000000000000..740c62e04bd38 --- /dev/null +++ b/test/other/embind_tsgen_ignore_2.d.ts @@ -0,0 +1,102 @@ +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { +} + +export interface Test { + x: number; + readonly y: number; + functionOne(_0: number, _1: number): number; + functionTwo(_0: number, _1: number): number; + functionFour(_0: boolean): number; + functionFive(x: number, y: number): number; + constFn(): number; + longFn(_0: number): number; + functionThree(_0: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): number; + functionSix(str: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): number; + delete(): void; +} + +export interface BarValue { + value: T; +} +export type Bar = BarValue<0>|BarValue<1>|BarValue<2>; + +export interface EmptyEnumValue { + value: T; +} +export type EmptyEnum = never/* Empty Enumerator */; + +export type ValArrIx = [ Bar, Bar, Bar, Bar ]; + +export interface IntVec { + push_back(_0: number): void; + resize(_0: number, _1: number): void; + size(): number; + set(_0: number, _1: number): boolean; + get(_0: number): any; + delete(): void; +} + +export interface Foo { + process(_0: Test): void; + delete(): void; +} + +export type ValObj = { + foo: Foo, + bar: Bar +}; + +export interface ClassWithConstructor { + fn(_0: number): number; + delete(): void; +} + +export interface ClassWithTwoConstructors { + delete(): void; +} + +export interface ClassWithSmartPtrConstructor { + fn(_0: number): number; + delete(): void; +} + +export interface BaseClass { + fn(_0: number): number; + delete(): void; +} + +export interface DerivedClass extends BaseClass { + fn2(_0: number): number; + delete(): void; +} + +export type ValArr = [ number, number, number ]; + +interface EmbindModule { + Test: {staticFunction(_0: number): number; staticFunctionWithParam(x: number): number; staticProperty: number}; + class_returning_fn(): Test; + class_unique_ptr_returning_fn(): Test; + a_class_instance: Test; + an_enum: Bar; + Bar: {valueOne: BarValue<0>, valueTwo: BarValue<1>, valueThree: BarValue<2>}; + EmptyEnum: {}; + enum_returning_fn(): Bar; + IntVec: {new(): IntVec}; + Foo: {}; + ClassWithConstructor: {new(_0: number, _1: ValArr): ClassWithConstructor}; + ClassWithTwoConstructors: {new(): ClassWithTwoConstructors; new(_0: number): ClassWithTwoConstructors}; + ClassWithSmartPtrConstructor: {new(_0: number, _1: ValArr): ClassWithSmartPtrConstructor}; + BaseClass: {}; + DerivedClass: {}; + a_bool: boolean; + an_int: number; + global_fn(_0: number, _1: number): number; + optional_test(_0: Foo | undefined): number | undefined; + smart_ptr_function(_0: ClassWithSmartPtrConstructor): number; + smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor): number; + function_with_callback_param(_0: (message: string) => void): number; + string_test(_0: ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string): string; + wstring_test(_0: string): string; +} +export type MainModule = WasmModule & EmbindModule; diff --git a/test/other/embind_tsgen_memory64.d.ts b/test/other/embind_tsgen_memory64.d.ts index e1119827cfd4e..1d1da69a7e1fe 100644 --- a/test/other/embind_tsgen_memory64.d.ts +++ b/test/other/embind_tsgen_memory64.d.ts @@ -1,3 +1,8 @@ -export interface MainModule { +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { +} + +interface EmbindModule { longFn(_0: bigint): bigint; } +export type MainModule = WasmModule & EmbindModule; diff --git a/test/other/test_emit_tsd.c b/test/other/test_emit_tsd.c new file mode 100644 index 0000000000000..1c8e0759c9ea7 --- /dev/null +++ b/test/other/test_emit_tsd.c @@ -0,0 +1,10 @@ +#include + +EMSCRIPTEN_KEEPALIVE void fooVoid() {} +EMSCRIPTEN_KEEPALIVE int fooInt(int a, int b) { + return 42; +} + +int main() { + +} diff --git a/test/other/test_emit_tsd.d.ts b/test/other/test_emit_tsd.d.ts new file mode 100644 index 0000000000000..d46ffb23369b8 --- /dev/null +++ b/test/other/test_emit_tsd.d.ts @@ -0,0 +1,8 @@ +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +interface WasmModule { + _fooVoid(): void; + _fooInt(_0: number, _1: number): number; + _main(_0: number, _1: number): number; +} + +export type MainModule = WasmModule; diff --git a/test/test_other.py b/test/test_other.py index 4a226a7b490bf..5ff29632f4a17 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3083,14 +3083,14 @@ def test_embind_tsgen_ignore(self): '-lembind', # Test duplicated link option. ] self.emcc(test_file('other/embind_tsgen.cpp'), extra_args) - self.assertFileContents(test_file('other/embind_tsgen.d.ts'), read_file('embind_tsgen.d.ts')) + self.assertFileContents(test_file('other/embind_tsgen_ignore_1.d.ts'), read_file('embind_tsgen.d.ts')) # Test these args separately since they conflict with arguments in the first test. extra_args = ['-sMODULARIZE', '--embed-file', 'fail.js', '-sMINIMAL_RUNTIME=2', '-sEXPORT_ES6=1'] self.emcc(test_file('other/embind_tsgen.cpp'), extra_args) - self.assertFileContents(test_file('other/embind_tsgen.d.ts'), read_file('embind_tsgen.d.ts')) + self.assertFileContents(test_file('other/embind_tsgen_ignore_2.d.ts'), read_file('embind_tsgen.d.ts')) def test_embind_tsgen_test_embind(self): self.run_process([EMCC, test_file('embind/embind_test.cpp'), @@ -3136,6 +3136,12 @@ def test_embind_jsgen_method_pointer_stability(self): # AOT JS generation still works correctly. self.do_runf('other/embind_jsgen_method_pointer_stability.cpp', 'done') + def test_emit_tsd(self): + self.run_process([EMCC, test_file('other/test_emit_tsd.c'), + '--emit-tsd', 'test_emit_tsd.d.ts', '-Wno-experimental'] + + self.get_emcc_args()) + self.assertFileContents(test_file('other/test_emit_tsd.d.ts'), read_file('test_emit_tsd.d.ts')) + def test_emconfig(self): output = self.run_process([emconfig, 'LLVM_ROOT'], stdout=PIPE).stdout.strip() self.assertEqual(output, config.LLVM_ROOT) diff --git a/tools/emscripten.py b/tools/emscripten.py index a5a38a3b1439f..42bf350772e7a 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -329,10 +329,10 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True): metadata.imports += ['__memory_base32'] if settings.ASYNCIFY == 1: - metadata.function_exports['asyncify_start_unwind'] = 1 - metadata.function_exports['asyncify_stop_unwind'] = 0 - metadata.function_exports['asyncify_start_rewind'] = 1 - metadata.function_exports['asyncify_stop_rewind'] = 0 + metadata.function_exports['asyncify_start_unwind'] = webassembly.FuncType([webassembly.Type.I32], []) + metadata.function_exports['asyncify_stop_unwind'] = webassembly.FuncType([], []) + metadata.function_exports['asyncify_start_rewind'] = webassembly.FuncType([webassembly.Type.I32], []) + metadata.function_exports['asyncify_stop_rewind'] = webassembly.FuncType([], []) # If the binary has already been finalized the settings have already been # updated and we can skip updating them. @@ -461,6 +461,8 @@ def emscript(in_wasm, out_wasm, outfile_js, js_syms, finalize=True): out.write(post) module = None + return metadata + @ToolchainProfiler.profile() def get_metadata(infile, outfile, modify_wasm, args): @@ -604,6 +606,34 @@ def finalize_wasm(infile, outfile, js_syms): return metadata +def create_tsd(metadata, embind_tsd): + function_exports = metadata.function_exports + out = '// TypeScript bindings for emscripten-generated code. Automatically generated at compile time.\n' + out += 'interface WasmModule {\n' + for name, types in function_exports.items(): + mangled = asmjs_mangle(name) + should_export = settings.EXPORT_KEEPALIVE and mangled in settings.EXPORTED_FUNCTIONS + if not should_export: + continue + arguments = [] + for index, type in enumerate(types.params): + arguments.append(f"_{index}: {type_to_ts_type(type)}") + out += f' {mangled}({", ".join(arguments)}): ' + assert len(types.returns) <= 1, 'One return type only supported' + if types.returns: + out += f'{type_to_ts_type(types.returns[0])}' + else: + out += 'void' + out += ';\n' + out += '}\n' + out += f'\n{embind_tsd}' + export_interfaces = 'WasmModule' + if embind_tsd: + export_interfaces += ' & EmbindModule' + out += f'export type MainModule = {export_interfaces};\n' + return out + + def create_asm_consts(metadata): asm_consts = {} for addr, const in metadata.asmConsts.items(): @@ -643,6 +673,17 @@ def type_to_sig(type): }[type] +def type_to_ts_type(type): + return { + webassembly.Type.I32: 'number', + webassembly.Type.I64: 'BigInt', + webassembly.Type.F32: 'number', + webassembly.Type.F64: 'number', + webassembly.Type.EXTERNREF: 'any', + webassembly.Type.VOID: 'void' + }[type] + + def func_type_to_sig(type): parameters = [type_to_sig(param) for param in type.params] if type.returns: @@ -794,7 +835,8 @@ def install_wrapper(sym): return False return True - for name, nargs in function_exports.items(): + for name, types in function_exports.items(): + nargs = len(types.params) mangled = asmjs_mangle(name) wrapper = 'var %s = ' % mangled diff --git a/tools/extract_metadata.py b/tools/extract_metadata.py index 2da50dfdbfa14..799151e1a51e8 100644 --- a/tools/extract_metadata.py +++ b/tools/extract_metadata.py @@ -258,7 +258,7 @@ def get_function_exports(module): rtn = {} for e in module.get_exports(): if e.kind == webassembly.ExternType.FUNC: - rtn[e.name] = len(module.get_function_type(e.index).params) + rtn[e.name] = module.get_function_type(e.index) return rtn diff --git a/tools/link.py b/tools/link.py index e4b0517b4d14b..8b45f61e8dbe2 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1832,13 +1832,13 @@ def phase_post_link(options, state, in_wasm, wasm_target, target, js_syms): settings.TARGET_JS_NAME = os.path.basename(state.js_target) - phase_emscript(options, in_wasm, wasm_target, js_syms) + metadata = phase_emscript(options, in_wasm, wasm_target, js_syms) if settings.EMBIND_AOT: phase_embind_aot(wasm_target, js_syms) - if options.embind_emit_tsd: - phase_embind_emit_tsd(options, wasm_target, js_syms) + if options.embind_emit_tsd or options.emit_tsd: + phase_emit_tsd(options, wasm_target, js_syms, metadata) if options.js_transform: phase_source_transforms(options) @@ -1865,8 +1865,9 @@ def phase_emscript(options, in_wasm, wasm_target, js_syms): if shared.SKIP_SUBPROCS: return - emscripten.emscript(in_wasm, wasm_target, final_js, js_syms) + metadata = emscripten.emscript(in_wasm, wasm_target, final_js, js_syms) save_intermediate('original') + return metadata def run_embind_gen(wasm_target, js_syms, extra_settings): @@ -1920,12 +1921,21 @@ def run_embind_gen(wasm_target, js_syms, extra_settings): return out -@ToolchainProfiler.profile_block('embind emit tsd') -def phase_embind_emit_tsd(options, wasm_target, js_syms): +@ToolchainProfiler.profile_block('emit tsd') +def phase_emit_tsd(options, wasm_target, js_syms, metadata): logger.debug('emit tsd') - out = run_embind_gen(wasm_target, js_syms, {'EMBIND_JS': False}) - out_file = os.path.join(os.path.dirname(wasm_target), options.embind_emit_tsd) - write_file(out_file, out) + filename = '' + # Support using either option for now, but prefer emit_tsd if specified. + if options.emit_tsd: + filename = options.emit_tsd + else: + filename = options.embind_emit_tsd + embind_tsd = '' + if settings.EMBIND: + embind_tsd = run_embind_gen(wasm_target, js_syms, {'EMBIND_JS': False}) + all_tsd = emscripten.create_tsd(metadata, embind_tsd) + out_file = os.path.join(os.path.dirname(wasm_target), filename) + write_file(out_file, all_tsd) @ToolchainProfiler.profile_block('embind aot js')