From be9c885e09557be628a3101c392766127fb9a9bc Mon Sep 17 00:00:00 2001
From: Brendan Dahl <brendan.dahl@gmail.com>
Date: Tue, 6 Feb 2024 21:54:27 +0000
Subject: [PATCH] Add option to emit TypeScript definitions for Wasm module
 exports.

The new flag `--emit-tsd <filename>` will generate a TypeScript defintion
file for any Wasm module exports. If embind is also used the definitions
for those types will also be included in the same file.

This still doesn't give the full picture of Wasm module e.g. missing
HEAP<N> and the various helper functions defined in JS.
---
 emcc.py                               |   4 +
 src/embind/embind_gen.js              |   2 +-
 test/other/embind_tsgen.d.ts          |   8 +-
 test/other/embind_tsgen_bigint.d.ts   |   7 +-
 test/other/embind_tsgen_ignore_1.d.ts | 110 ++++++++++++++++++++++++++
 test/other/embind_tsgen_ignore_2.d.ts | 102 ++++++++++++++++++++++++
 test/other/embind_tsgen_memory64.d.ts |   7 +-
 test/other/test_emit_tsd.c            |  10 +++
 test/other/test_emit_tsd.d.ts         |   8 ++
 test/test_other.py                    |  10 ++-
 tools/emscripten.py                   |  52 ++++++++++--
 tools/extract_metadata.py             |   2 +-
 tools/link.py                         |  28 ++++---
 13 files changed, 329 insertions(+), 21 deletions(-)
 create mode 100644 test/other/embind_tsgen_ignore_1.d.ts
 create mode 100644 test/other/embind_tsgen_ignore_2.d.ts
 create mode 100644 test/other/test_emit_tsd.c
 create mode 100644 test/other/test_emit_tsd.d.ts

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<T extends number> {
+  value: T;
+}
+export type Bar = BarValue<0>|BarValue<1>|BarValue<2>;
+
+export interface EmptyEnumValue<T extends number> {
+  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<T extends number> {
+  value: T;
+}
+export type Bar = BarValue<0>|BarValue<1>|BarValue<2>;
+
+export interface EmptyEnumValue<T extends number> {
+  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.h>
+
+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')