From a3433a8b9cef8f07d1e285656d3dfd7256fdbf91 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 18 Sep 2024 17:46:36 +0000 Subject: [PATCH 1/2] [tsgen] Expand TS generation test to compile and run a TS file. In the new TS file we can add tests for things where just checking if the definition file compiles is not enough. This will help catch issues like #22569. --- test/other/embind_tsgen.cpp | 13 +++++++++++-- test/other/embind_tsgen.d.ts | 14 ++++++++------ test/other/embind_tsgen.ts | 28 +++++++++++++++++++++++++++ test/other/embind_tsgen_ignore_1.d.ts | 14 ++++++++------ test/other/embind_tsgen_ignore_2.d.ts | 14 ++++++++------ test/other/embind_tsgen_ignore_3.d.ts | 14 ++++++++------ test/other/embind_tsgen_package.json | 13 +++++++++++++ test/test_other.py | 10 +++++++--- 8 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 test/other/embind_tsgen.ts create mode 100644 test/other/embind_tsgen_package.json diff --git a/test/other/embind_tsgen.cpp b/test/other/embind_tsgen.cpp index 8687f8e5dc49..66bfe440d86a 100644 --- a/test/other/embind_tsgen.cpp +++ b/test/other/embind_tsgen.cpp @@ -58,12 +58,19 @@ struct ValArr { EMSCRIPTEN_DECLARE_VAL_TYPE(CallbackType); struct ValObj { - Foo foo; Bar bar; + std::string string; CallbackType callback; ValObj() : callback(val::undefined()) {} }; +ValObj getValObj() { + ValObj o; + return o; +} + +void setValObj(ValObj v) {} + class ClassWithConstructor { public: ClassWithConstructor(int, const ValArr&) {} @@ -190,9 +197,11 @@ EMSCRIPTEN_BINDINGS(Test) { .element(emscripten::index<3>()); value_object("ValObj") - .field("foo", &ValObj::foo) + .field("string", &ValObj::string) .field("bar", &ValObj::bar) .field("callback", &ValObj::callback); + function("getValObj", &getValObj); + function("setValObj", &setValObj); register_vector("IntVec"); diff --git a/test/other/embind_tsgen.d.ts b/test/other/embind_tsgen.d.ts index 1ec9a753088b..7da0474454a6 100644 --- a/test/other/embind_tsgen.d.ts +++ b/test/other/embind_tsgen.d.ts @@ -85,12 +85,6 @@ export interface ClassWithSmartPtrConstructor { delete(): void; } -export type ValObj = { - foo: Foo, - bar: Bar, - callback: (message: string) => void -}; - export interface BaseClass { fn(_0: number): number; delete(): void; @@ -113,6 +107,12 @@ export interface InterfaceWrapper extends Interface { export type ValArr = [ number, number, number ]; +export type ValObj = { + string: EmbindString, + bar: Bar, + callback: (message: string) => void +}; + interface EmbindModule { Test: { staticFunction(_0: number): number; @@ -163,6 +163,8 @@ interface EmbindModule { smart_ptr_function(_0: ClassWithSmartPtrConstructor | null): number; smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor | null): number; function_with_callback_param(_0: (message: string) => void): number; + getValObj(): ValObj; + setValObj(_0: ValObj): void; string_test(_0: EmbindString): string; wstring_test(_0: string): string; } diff --git a/test/other/embind_tsgen.ts b/test/other/embind_tsgen.ts new file mode 100644 index 000000000000..eecdccdb5f7c --- /dev/null +++ b/test/other/embind_tsgen.ts @@ -0,0 +1,28 @@ + +import moduleLoader from './embind_tsgen.mjs'; + +const module = await moduleLoader(); + +// Test a few variations of passing value_objects with strings. +module.setValObj({ + bar: module.Bar.valueOne, + string: "ABCD", + callback: () => {} +}); + +module.setValObj({ + bar: module.Bar.valueOne, + string: new Int8Array([65, 66, 67, 68]), + callback: () => {} +}); + +const valObj = module.getValObj(); +// TODO: remove the cast below when better definitions are generated for value +// objects. +const valString : string = valObj.string as string; + +// Ensure nonnull pointers do no need a cast or nullptr check to use. +const obj = module.getNonnullPointer(); +obj.delete(); + +console.log('ts ran'); diff --git a/test/other/embind_tsgen_ignore_1.d.ts b/test/other/embind_tsgen_ignore_1.d.ts index 47618dc6dc20..7fab6255ae6d 100644 --- a/test/other/embind_tsgen_ignore_1.d.ts +++ b/test/other/embind_tsgen_ignore_1.d.ts @@ -94,12 +94,6 @@ export interface ClassWithSmartPtrConstructor { delete(): void; } -export type ValObj = { - foo: Foo, - bar: Bar, - callback: (message: string) => void -}; - export interface BaseClass { fn(_0: number): number; delete(): void; @@ -122,6 +116,12 @@ export interface InterfaceWrapper extends Interface { export type ValArr = [ number, number, number ]; +export type ValObj = { + string: EmbindString, + bar: Bar, + callback: (message: string) => void +}; + interface EmbindModule { Test: { staticFunction(_0: number): number; @@ -172,6 +172,8 @@ interface EmbindModule { smart_ptr_function(_0: ClassWithSmartPtrConstructor | null): number; smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor | null): number; function_with_callback_param(_0: (message: string) => void): number; + getValObj(): ValObj; + setValObj(_0: ValObj): void; string_test(_0: EmbindString): string; wstring_test(_0: string): string; } diff --git a/test/other/embind_tsgen_ignore_2.d.ts b/test/other/embind_tsgen_ignore_2.d.ts index 3d4cce80a2a2..08f2b4da00a9 100644 --- a/test/other/embind_tsgen_ignore_2.d.ts +++ b/test/other/embind_tsgen_ignore_2.d.ts @@ -71,12 +71,6 @@ export interface ClassWithSmartPtrConstructor { delete(): void; } -export type ValObj = { - foo: Foo, - bar: Bar, - callback: (message: string) => void -}; - export interface BaseClass { fn(_0: number): number; delete(): void; @@ -99,6 +93,12 @@ export interface InterfaceWrapper extends Interface { export type ValArr = [ number, number, number ]; +export type ValObj = { + string: EmbindString, + bar: Bar, + callback: (message: string) => void +}; + interface EmbindModule { Test: { staticFunction(_0: number): number; @@ -149,6 +149,8 @@ interface EmbindModule { smart_ptr_function(_0: ClassWithSmartPtrConstructor | null): number; smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor | null): number; function_with_callback_param(_0: (message: string) => void): number; + getValObj(): ValObj; + setValObj(_0: ValObj): void; string_test(_0: EmbindString): string; wstring_test(_0: string): string; } diff --git a/test/other/embind_tsgen_ignore_3.d.ts b/test/other/embind_tsgen_ignore_3.d.ts index 1ec9a753088b..7da0474454a6 100644 --- a/test/other/embind_tsgen_ignore_3.d.ts +++ b/test/other/embind_tsgen_ignore_3.d.ts @@ -85,12 +85,6 @@ export interface ClassWithSmartPtrConstructor { delete(): void; } -export type ValObj = { - foo: Foo, - bar: Bar, - callback: (message: string) => void -}; - export interface BaseClass { fn(_0: number): number; delete(): void; @@ -113,6 +107,12 @@ export interface InterfaceWrapper extends Interface { export type ValArr = [ number, number, number ]; +export type ValObj = { + string: EmbindString, + bar: Bar, + callback: (message: string) => void +}; + interface EmbindModule { Test: { staticFunction(_0: number): number; @@ -163,6 +163,8 @@ interface EmbindModule { smart_ptr_function(_0: ClassWithSmartPtrConstructor | null): number; smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor | null): number; function_with_callback_param(_0: (message: string) => void): number; + getValObj(): ValObj; + setValObj(_0: ValObj): void; string_test(_0: EmbindString): string; wstring_test(_0: string): string; } diff --git a/test/other/embind_tsgen_package.json b/test/other/embind_tsgen_package.json new file mode 100644 index 000000000000..72b9328fe01a --- /dev/null +++ b/test/other/embind_tsgen_package.json @@ -0,0 +1,13 @@ +{ + "name": "index", + "version": "1.0.0", + "description": "", + "main": "main.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "" +} diff --git a/test/test_other.py b/test/test_other.py index bd69c2249833..0a4c6f601ca9 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3365,12 +3365,16 @@ def test_jspi_add_function(self): def test_embind_tsgen(self, opts): # Check that TypeScript generation works and that the program is runs as # expected. - self.do_runf('other/embind_tsgen.cpp', 'main ran', - emcc_args=['-lembind', '--emit-tsd', 'embind_tsgen.d.ts'] + opts) + self.emcc(test_file('other/embind_tsgen.cpp'), + ['-o', 'embind_tsgen.mjs', '-lembind', '--emit-tsd', 'embind_tsgen.d.ts'] + opts) # Test that the output compiles with a TS file that uses the defintions. - cmd = shared.get_npm_cmd('tsc') + ['embind_tsgen.d.ts', '--noEmit'] + shutil.copyfile(test_file('other/embind_tsgen.ts'), 'main.ts') + # A package file with type=module is needed to allow top level await in TS. + shutil.copyfile(test_file('other/embind_tsgen_package.json'), 'package.json') + cmd = shared.get_npm_cmd('tsc') + ['embind_tsgen.d.ts', 'main.ts', '--module', 'NodeNext', '--moduleResolution', 'nodenext'] shared.check_call(cmd) + self.assertContained('main ran\nts ran', self.run_js('main.js')) actual = read_file('embind_tsgen.d.ts') self.assertFileContents(test_file('other/embind_tsgen.d.ts'), actual) From 7e3c1c7e43df00914592c568333d7ed6cdba9198 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 18 Sep 2024 21:13:29 +0000 Subject: [PATCH 2/2] - Address comments - Use different expected test file now that the output is different. - --- .../{embind_tsgen.ts => embind_tsgen_main.ts} | 8 +- test/other/embind_tsgen_module.d.ts | 173 ++++++++++++++++++ test/other/embind_tsgen_package.json | 12 +- test/test_other.py | 7 +- 4 files changed, 183 insertions(+), 17 deletions(-) rename test/other/{embind_tsgen.ts => embind_tsgen_main.ts} (70%) create mode 100644 test/other/embind_tsgen_module.d.ts diff --git a/test/other/embind_tsgen.ts b/test/other/embind_tsgen_main.ts similarity index 70% rename from test/other/embind_tsgen.ts rename to test/other/embind_tsgen_main.ts index eecdccdb5f7c..a3a39683245a 100644 --- a/test/other/embind_tsgen.ts +++ b/test/other/embind_tsgen_main.ts @@ -1,7 +1,9 @@ +// Example TS program that consumes the emscripten-generated module to to +// illustrate how the type definitions are used and test they are workings as +// expected. +import moduleFactory from './embind_tsgen.mjs'; -import moduleLoader from './embind_tsgen.mjs'; - -const module = await moduleLoader(); +const module = await moduleFactory(); // Test a few variations of passing value_objects with strings. module.setValObj({ diff --git a/test/other/embind_tsgen_module.d.ts b/test/other/embind_tsgen_module.d.ts new file mode 100644 index 000000000000..ad5c291f7114 --- /dev/null +++ b/test/other/embind_tsgen_module.d.ts @@ -0,0 +1,173 @@ +// TypeScript bindings for emscripten-generated code. Automatically generated at compile time. +declare namespace RuntimeExports { + let HEAPF32: any; + let HEAPF64: any; + let HEAP_DATA_VIEW: any; + let HEAP8: any; + let HEAPU8: any; + let HEAP16: any; + let HEAPU16: any; + let HEAP32: any; + let HEAPU32: any; + let HEAP64: any; + let HEAPU64: any; +} +interface WasmModule { + _main(_0: number, _1: number): number; +} + +type EmbindString = ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string; +export interface Test { + x: number; + readonly y: number; + get stringProperty(): string; + set stringProperty(value: EmbindString); + 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: EmbindString): number; + functionSix(str: EmbindString): number; + delete(): void; +} + +export interface Obj { + 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; + get(_0: number): number | undefined; + set(_0: number, _1: number): boolean; + delete(): void; +} + +export interface MapIntInt { + keys(): IntVec; + get(_0: number): number | undefined; + set(_0: number, _1: number): void; + size(): number; + delete(): void; +} + +export interface Foo { + process(_0: Test): void; + delete(): void; +} + +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 interface Interface { + invoke(_0: EmbindString): void; + delete(): void; +} + +export interface InterfaceWrapper extends Interface { + notifyOnDestruction(): void; + delete(): void; +} + +export type ValArr = [ number, number, number ]; + +export type ValObj = { + string: EmbindString, + bar: Bar, + callback: (message: string) => void +}; + +interface EmbindModule { + Test: { + staticFunction(_0: number): number; + staticFunctionWithParam(x: number): number; + staticProperty: number; + get staticStringProperty(): string; + set staticStringProperty(value: EmbindString); + }; + class_returning_fn(): Test; + class_unique_ptr_returning_fn(): Test; + Obj: {}; + getPointer(_0: Obj | null): Obj | null; + getNonnullPointer(): Obj; + 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; + }; + MapIntInt: { + new(): MapIntInt; + }; + Foo: {}; + ClassWithConstructor: { + new(_0: number, _1: ValArr): ClassWithConstructor; + }; + ClassWithTwoConstructors: { + new(): ClassWithTwoConstructors; + new(_0: number): ClassWithTwoConstructors; + }; + ClassWithSmartPtrConstructor: { + new(_0: number, _1: ValArr): ClassWithSmartPtrConstructor; + }; + BaseClass: {}; + DerivedClass: {}; + Interface: { + implement(_0: any): InterfaceWrapper; + extend(_0: EmbindString, _1: any): any; + }; + InterfaceWrapper: {}; + a_bool: boolean; + an_int: number; + optional_test(_0?: Foo): number | undefined; + global_fn(_0: number, _1: number): number; + optional_and_nonoptional_test(_0: Foo | undefined, _1: number): number | undefined; + smart_ptr_function(_0: ClassWithSmartPtrConstructor | null): number; + smart_ptr_function_with_params(foo: ClassWithSmartPtrConstructor | null): number; + function_with_callback_param(_0: (message: string) => void): number; + getValObj(): ValObj; + setValObj(_0: ValObj): void; + string_test(_0: EmbindString): string; + wstring_test(_0: string): string; +} + +export type MainModule = WasmModule & typeof RuntimeExports & EmbindModule; +export default function MainModuleFactory (options?: unknown): Promise; diff --git a/test/other/embind_tsgen_package.json b/test/other/embind_tsgen_package.json index 72b9328fe01a..3dbc1ca591c0 100644 --- a/test/other/embind_tsgen_package.json +++ b/test/other/embind_tsgen_package.json @@ -1,13 +1,3 @@ { - "name": "index", - "version": "1.0.0", - "description": "", - "main": "main.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "" + "type": "module" } diff --git a/test/test_other.py b/test/test_other.py index 0a4c6f601ca9..a5157b810099 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3369,15 +3369,16 @@ def test_embind_tsgen(self, opts): ['-o', 'embind_tsgen.mjs', '-lembind', '--emit-tsd', 'embind_tsgen.d.ts'] + opts) # Test that the output compiles with a TS file that uses the defintions. - shutil.copyfile(test_file('other/embind_tsgen.ts'), 'main.ts') - # A package file with type=module is needed to allow top level await in TS. + shutil.copyfile(test_file('other/embind_tsgen_main.ts'), 'main.ts') + # A package file with type=module is needed to tell the TSC that we're + # using modules. shutil.copyfile(test_file('other/embind_tsgen_package.json'), 'package.json') cmd = shared.get_npm_cmd('tsc') + ['embind_tsgen.d.ts', 'main.ts', '--module', 'NodeNext', '--moduleResolution', 'nodenext'] shared.check_call(cmd) self.assertContained('main ran\nts ran', self.run_js('main.js')) actual = read_file('embind_tsgen.d.ts') - self.assertFileContents(test_file('other/embind_tsgen.d.ts'), actual) + self.assertFileContents(test_file('other/embind_tsgen_module.d.ts'), actual) def test_embind_tsgen_ignore(self): create_file('fail.js', 'assert(false);')