Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to emit TypeScript definitions for Wasm module exports. #21279

Merged
merged 1 commit into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't added docs for this yet. I'd like to do a little more work on it before we advertise it, but if the reviewer prefers I can add them now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case it might be worth marking it as experimental by doing something like we do for wasm64: diagnostics.warning('experimental', '-sMEMORY64 is still experimental. Many features may not work.')

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'):
Expand Down
2 changes: 1 addition & 1 deletion src/embind/embind_gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion test/other/embind_tsgen.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// TypeScript bindings for emscripten-generated code. Automatically generated at compile time.
interface WasmModule {
_main(_0: number, _1: number): number;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a followup we could try to give better name here perhaps? Maybe for well known functions like _main, _malloc and _free we could even just hardcode them?

}

export interface Test {
x: number;
readonly y: number;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
7 changes: 6 additions & 1 deletion test/other/embind_tsgen_bigint.d.ts
Original file line number Diff line number Diff line change
@@ -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;
110 changes: 110 additions & 0 deletions test/other/embind_tsgen_ignore_1.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of cool to see these like this.

I think it might be good if we could ensure that the --rebase setting works for these? I think it probably just works.

}

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;
102 changes: 102 additions & 0 deletions test/other/embind_tsgen_ignore_2.d.ts
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 6 additions & 1 deletion test/other/embind_tsgen_memory64.d.ts
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions test/other/test_emit_tsd.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE void fooVoid() {}
EMSCRIPTEN_KEEPALIVE int fooInt(int a, int b) {
return 42;
}

int main() {

}
8 changes: 8 additions & 0 deletions test/other/test_emit_tsd.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// TypeScript bindings for emscripten-generated code. Automatically generated at compile time.
interface WasmModule {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a comment to the of this file?

Something like // TypeScript bindings for emscripten-generate code. Automatically generated at compile time.?

_fooVoid(): void;
_fooInt(_0: number, _1: number): number;
_main(_0: number, _1: number): number;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!


export type MainModule = WasmModule;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does MainModule from?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you mean where the name comes from our where it's defined. MainModule is defined here, it's basically an alias to WasmModule. The name is what embind previously used, so I'm sticking with that for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough for now. I'm not sure why we need alias though.. why not just call it Module (or what ever the EXPORT_NAME is?)

10 changes: 8 additions & 2 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What rename these? How do they differ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have different options enabled which changes the output of the wasm module exports.

# 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'),
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading