diff options
author | Pavel Savara <pavel.savara@gmail.com> | 2022-07-10 22:40:36 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-10 22:40:36 +0300 |
commit | 8aa77a952a285dcd318cf8e6c99ba6f99cec897f (patch) | |
tree | 68f905ecf753a40fda259ccc1772d33c202aa455 /src/mono/wasm/runtime | |
parent | 31cbf121d396ab0b70a6e7c46cbdfb7cdbd841e2 (diff) |
[wasm]: JavaScript interop with [JSImport] and [JSExport] attributes and Roslyn (#66304)
Co-authored-by: Marek FiĊĦera <mara@neptuo.com>
Co-authored-by: Katelyn Gadd <kg@luminance.org>
Diffstat (limited to 'src/mono/wasm/runtime')
21 files changed, 2753 insertions, 361 deletions
diff --git a/src/mono/wasm/runtime/cancelable-promise.ts b/src/mono/wasm/runtime/cancelable-promise.ts index ebd01a86e5b..422d3b1ff66 100644 --- a/src/mono/wasm/runtime/cancelable-promise.ts +++ b/src/mono/wasm/runtime/cancelable-promise.ts @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { mono_wasm_get_jsobj_from_js_handle } from "./gc-handles"; -import { wrap_error_root } from "./method-calls"; -import { mono_wasm_new_external_root } from "./roots"; -import { JSHandle, MonoObject, MonoObjectRef, MonoString } from "./types"; -import { Int32Ptr } from "./types/emscripten"; +import { _lookup_js_owned_object } from "./gc-handles"; +import { TaskCallbackHolder } from "./marshal-to-cs"; +import { mono_assert, GCHandle } from "./types"; export const _are_promises_supported = ((typeof Promise === "object") || (typeof Promise === "function")) && (typeof Promise.resolve === "function"); export const promise_control_symbol = Symbol.for("wasm promise_control"); @@ -18,34 +16,20 @@ export function isThenable(js_obj: any): boolean { ((typeof js_obj === "object" || typeof js_obj === "function") && typeof js_obj.then === "function"); } -export function mono_wasm_cancel_promise_ref(thenable_js_handle: JSHandle, is_exception: Int32Ptr, result_address: MonoObjectRef): void | MonoString { - const resultRoot = mono_wasm_new_external_root<MonoObject>(result_address); - try { - const promise = mono_wasm_get_jsobj_from_js_handle(thenable_js_handle); - const promise_control = promise[promise_control_symbol]; - promise_control.reject("OperationCanceledException"); - } - catch (ex) { - wrap_error_root(is_exception, ex, resultRoot); - return; - } - finally { - resultRoot.release(); - } -} - export interface PromiseControl { isDone: boolean; resolve: (data?: any) => void; reject: (reason: any) => void; + promise: Promise<any>; } -export function _create_cancelable_promise(afterResolve?: () => void, afterReject?: () => void): { +export function create_cancelable_promise(afterResolve?: () => void, afterReject?: () => void): { promise: Promise<any>, promise_control: PromiseControl } { - let promise_control: PromiseControl | null = null; + let promise_control: PromiseControl = <any>null; const promise = new Promise(function (resolve, reject) { promise_control = { + promise: <any>null, isDone: false, resolve: (data: any) => { if (!promise_control!.isDone) { @@ -67,6 +51,26 @@ export function _create_cancelable_promise(afterResolve?: () => void, afterRejec } }; }); + promise_control.promise = promise; (<any>promise)[promise_control_symbol] = promise_control; return { promise, promise_control: promise_control! }; -}
\ No newline at end of file +} + +export function wrap_as_cancelable_promise<T>(fn: () => Promise<T>): Promise<T> { + const { promise, promise_control } = create_cancelable_promise(); + const inner = fn(); + inner.then((data) => promise_control.resolve(data)).catch((reason) => promise_control.reject(reason)); + return promise; +} + +export function mono_wasm_cancel_promise(task_holder_gc_handle: GCHandle): void { + const holder = _lookup_js_owned_object(task_holder_gc_handle) as TaskCallbackHolder; + if (!holder) return; // probably already GC collected + + const promise = holder.promise; + mono_assert(!!promise, () => `Expected Promise for GCHandle ${task_holder_gc_handle}`); + const promise_control = (<any>promise)[promise_control_symbol]; + mono_assert(!!promise_control, () => `Expected promise_control for GCHandle ${task_holder_gc_handle}`); + promise_control.reject("OperationCanceledException"); +} + diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 3f8a0c56cda..93aaa3a2e24 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -74,13 +74,11 @@ const linked_functions = [ "mono_wasm_typed_array_copy_to_ref", "mono_wasm_typed_array_from_ref", "mono_wasm_typed_array_copy_from_ref", - "mono_wasm_cancel_promise_ref", - "mono_wasm_web_socket_open_ref", - "mono_wasm_web_socket_send", - "mono_wasm_web_socket_receive", - "mono_wasm_web_socket_close_ref", - "mono_wasm_web_socket_abort", "mono_wasm_compile_function_ref", + "mono_wasm_bind_js_function", + "mono_wasm_invoke_bound_function", + "mono_wasm_bind_cs_function", + "mono_wasm_marshal_promise", // pal_icushim_static.c "mono_wasm_load_icu_data", diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index 2313924d9b9..11bd47ee8a3 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -29,12 +29,12 @@ extern void mono_wasm_typed_array_to_array_ref (int js_handle, int *is_exception extern void mono_wasm_typed_array_copy_to_ref (int js_handle, int ptr, int begin, int end, int bytes_per_element, int *is_exception, MonoObject** result); extern void mono_wasm_typed_array_from_ref (int ptr, int begin, int end, int bytes_per_element, int type, int *is_exception, MonoObject** result); extern void mono_wasm_typed_array_copy_from_ref (int js_handle, int ptr, int begin, int end, int bytes_per_element, int *is_exception, MonoObject** result); -extern void mono_wasm_cancel_promise_ref (int thenable_js_handle, int *is_exception, MonoString** result); -extern void mono_wasm_web_socket_open_ref (MonoString **uri, MonoArray **subProtocols, MonoDelegate **on_close, int *web_socket_js_handle, int *thenable_js_handle, int *is_exception, MonoObject **result); -extern void mono_wasm_web_socket_send (int webSocket_js_handle, void* buffer_ptr, int offset, int length, int message_type, int end_of_message, int *thenable_js_handle, int *is_exception, MonoObject **result); -extern void mono_wasm_web_socket_receive (int webSocket_js_handle, void* buffer_ptr, int offset, int length, void* response_ptr, int *thenable_js_handle, int *is_exception, MonoObject **result); -extern void mono_wasm_web_socket_close_ref (int webSocket_js_handle, int code, MonoString **reason, int wait_for_close_received, int *thenable_js_handle, int *is_exception, MonoObject **result); -extern void mono_wasm_web_socket_abort (int webSocket_js_handle, int *is_exception, MonoString **result); + +extern void mono_wasm_bind_js_function(MonoString **function_name, MonoString **module_name, void *signature, int* function_js_handle, int *is_exception, MonoObject **result); +extern void mono_wasm_invoke_bound_function(int function_js_handle, void *data); +extern void mono_wasm_bind_cs_function(MonoString **fully_qualified_name, int signature_hash, void* signatures, int *is_exception, MonoObject **result); +extern void mono_wasm_marshal_promise(void *data); + void core_initialize_internals () { @@ -50,12 +50,13 @@ void core_initialize_internals () mono_add_internal_call ("Interop/Runtime::TypedArrayCopyToRef", mono_wasm_typed_array_copy_to_ref); mono_add_internal_call ("Interop/Runtime::TypedArrayFromRef", mono_wasm_typed_array_from_ref); mono_add_internal_call ("Interop/Runtime::TypedArrayCopyFromRef", mono_wasm_typed_array_copy_from_ref); - mono_add_internal_call ("Interop/Runtime::WebSocketOpenRef", mono_wasm_web_socket_open_ref); - mono_add_internal_call ("Interop/Runtime::WebSocketSend", mono_wasm_web_socket_send); - mono_add_internal_call ("Interop/Runtime::WebSocketReceive", mono_wasm_web_socket_receive); - mono_add_internal_call ("Interop/Runtime::WebSocketCloseRef", mono_wasm_web_socket_close_ref); - mono_add_internal_call ("Interop/Runtime::WebSocketAbort", mono_wasm_web_socket_abort); - mono_add_internal_call ("Interop/Runtime::CancelPromiseRef", mono_wasm_cancel_promise_ref); + + mono_add_internal_call ("Interop/Runtime::BindJSFunction", mono_wasm_bind_js_function); + mono_add_internal_call ("Interop/Runtime::InvokeJSFunction", mono_wasm_invoke_bound_function); + mono_add_internal_call ("Interop/Runtime::BindCSFunction", mono_wasm_bind_cs_function); + mono_add_internal_call ("Interop/Runtime::MarshalPromise", mono_wasm_marshal_promise); + mono_add_internal_call ("Interop/Runtime::RegisterGCRoot", mono_wasm_register_root); + mono_add_internal_call ("Interop/Runtime::DeregisterGCRoot", mono_wasm_deregister_root); } // Int8Array | int8_t | byte or SByte (signed byte) diff --git a/src/mono/wasm/runtime/corebindings.ts b/src/mono/wasm/runtime/corebindings.ts index 82be21ce2a2..bd9c0fb12eb 100644 --- a/src/mono/wasm/runtime/corebindings.ts +++ b/src/mono/wasm/runtime/corebindings.ts @@ -5,6 +5,7 @@ import { JSHandle, GCHandle, MonoObjectRef } from "./types"; import { PromiseControl } from "./cancelable-promise"; import { runtimeHelpers } from "./imports"; +// TODO replace all of this with [JSExport] const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMarshalString*/][] = [ ["_get_cs_owned_object_by_js_handle_ref", "GetCSOwnedObjectByJSHandleRef", "iim"], ["_get_cs_owned_object_js_handle_ref", "GetCSOwnedObjectJSHandleRef", "mi"], @@ -16,6 +17,7 @@ const fn_signatures: [jsname: string, csname: string, signature: string/*ArgsMar ["_release_js_owned_object_by_gc_handle", "ReleaseJSOwnedObjectByGCHandle", "i"], ["_create_tcs", "CreateTaskSource", ""], + ["_create_task_callback", "CreateTaskCallback", ""], ["_set_tcs_result_ref", "SetTaskSourceResultRef", "iR"], ["_set_tcs_failure", "SetTaskSourceFailure", "is"], ["_get_tcs_task_ref", "GetTaskSourceTaskRef", "im"], @@ -53,6 +55,8 @@ export interface t_CSwraps { _create_date_time_ref(ticks: number, result: MonoObjectRef): void; _create_uri_ref(uri: string, result: MonoObjectRef): void; _is_simple_array_ref(obj: MonoObjectRef): boolean; + + _create_task_callback(): GCHandle; } const wrapped_cs_functions: t_CSwraps = <any>{}; diff --git a/src/mono/wasm/runtime/cs-to-js.ts b/src/mono/wasm/runtime/cs-to-js.ts index 9f5e583dbdb..0f5392ea880 100644 --- a/src/mono/wasm/runtime/cs-to-js.ts +++ b/src/mono/wasm/runtime/cs-to-js.ts @@ -14,9 +14,10 @@ import cwraps from "./cwraps"; import { get_js_owned_object_by_gc_handle_ref, js_owned_gc_handle_symbol, mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle, setup_managed_proxy, teardown_managed_proxy, _lookup_js_owned_object } from "./gc-handles"; import { mono_method_get_call_signature_ref, call_method_ref, wrap_error_root } from "./method-calls"; import { js_to_mono_obj_root } from "./js-to-cs"; -import { _are_promises_supported, _create_cancelable_promise } from "./cancelable-promise"; +import { _are_promises_supported, create_cancelable_promise } from "./cancelable-promise"; import { getU32, getI32, getF32, getF64 } from "./memory"; import { Int32Ptr, VoidPtr } from "./types/emscripten"; +import { ManagedObject } from "./marshal"; const delegate_invoke_symbol = Symbol.for("wasm delegate_invoke"); const delegate_invoke_signature_symbol = Symbol.for("wasm delegate_invoke_signature"); @@ -296,7 +297,7 @@ function _unbox_task_root_as_promise(root: WasmRoot<MonoObject>) { if (!result) { const explicitFinalization = () => teardown_managed_proxy(result, gc_handle); - const { promise, promise_control } = _create_cancelable_promise(explicitFinalization, explicitFinalization); + const { promise, promise_control } = create_cancelable_promise(explicitFinalization, explicitFinalization); // note that we do not implement promise/task roundtrip // With more complexity we could recover original instance when this promise is marshaled back to C#. @@ -335,7 +336,7 @@ export function _unbox_ref_type_root_as_js_object(root: WasmRoot<MonoObject>): a // If the JS object for this gc_handle was already collected (or was never created) if (is_nullish(result)) { - result = {}; + result = new ManagedObject(); // keep the gc_handle so that we could easily convert it back to original C# object for roundtrip result[js_owned_gc_handle_symbol] = gc_handle; diff --git a/src/mono/wasm/runtime/cwraps.ts b/src/mono/wasm/runtime/cwraps.ts index c4892f33dbf..b2963790bd2 100644 --- a/src/mono/wasm/runtime/cwraps.ts +++ b/src/mono/wasm/runtime/cwraps.ts @@ -8,6 +8,7 @@ import { MonoType, MonoObjectRef, MonoStringRef } from "./types"; import { Module } from "./imports"; +import { JSMarshalerArguments } from "./marshal"; import { VoidPtr, CharPtrPtr, Int32Ptr, CharPtr, ManagedPointer } from "./types/emscripten"; const fn_signatures: [ident: string, returnType: string | null, argTypes?: string[], opts?: any][] = [ @@ -37,6 +38,7 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_assembly_load", "number", ["string"]], ["mono_wasm_find_corlib_class", "number", ["string", "string"]], ["mono_wasm_assembly_find_class", "number", ["number", "string", "string"]], + ["mono_wasm_runtime_run_module_cctor", "void", ["number"]], ["mono_wasm_find_corlib_type", "number", ["string", "string"]], ["mono_wasm_assembly_find_type", "number", ["number", "string", "string"]], ["mono_wasm_assembly_find_method", "number", ["number", "string", "number"]], @@ -79,6 +81,7 @@ const fn_signatures: [ident: string, returnType: string | null, argTypes?: strin ["mono_wasm_enable_on_demand_gc", "void", ["number"]], ["mono_profiler_init_aot", "void", ["number"]], ["mono_wasm_exec_regression", "number", ["number", "string"]], + ["mono_wasm_invoke_method_bound", "number", ["number", "number"]], ["mono_wasm_write_managed_pointer_unsafe", "void", ["number", "number"]], ["mono_wasm_copy_managed_pointer", "void", ["number", "number"]], ["mono_wasm_i52_to_f64", "number", ["number", "number"]], @@ -182,12 +185,14 @@ export interface t_Cwraps { mono_wasm_set_main_args(argc: number, argv: VoidPtr): void; mono_profiler_init_aot(desc: string): void; mono_wasm_exec_regression(verbose_level: number, image: string): number; + mono_wasm_invoke_method_bound(method: MonoMethod, args: JSMarshalerArguments): MonoString; mono_wasm_write_managed_pointer_unsafe(destination: VoidPtr | MonoObjectRef, pointer: ManagedPointer): void; mono_wasm_copy_managed_pointer(destination: VoidPtr | MonoObjectRef, source: VoidPtr | MonoObjectRef): void; - mono_wasm_i52_to_f64 (source: VoidPtr, error: Int32Ptr) : number; - mono_wasm_u52_to_f64 (source: VoidPtr, error: Int32Ptr) : number; - mono_wasm_f64_to_i52 (destination: VoidPtr, value: number) : I52Error; - mono_wasm_f64_to_u52 (destination: VoidPtr, value: number) : I52Error; + mono_wasm_i52_to_f64(source: VoidPtr, error: Int32Ptr): number; + mono_wasm_u52_to_f64(source: VoidPtr, error: Int32Ptr): number; + mono_wasm_f64_to_i52(destination: VoidPtr, value: number): I52Error; + mono_wasm_f64_to_u52(destination: VoidPtr, value: number): I52Error; + mono_wasm_runtime_run_module_cctor(assembly: MonoAssembly): void; } const wrapped_c_functions: t_Cwraps = <any>{}; diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 0f2110f53d0..dd58eb3e3f2 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -376,6 +376,49 @@ declare function getF64(offset: _MemOffset): number; declare function mono_run_main_and_exit(main_assembly_name: string, args: string[]): Promise<void>; declare function mono_run_main(main_assembly_name: string, args: string[]): Promise<number>; +interface IDisposable { + dispose(): void; + get isDisposed(): boolean; +} +declare class ManagedObject implements IDisposable { + dispose(): void; + get isDisposed(): boolean; + toString(): string; +} +declare class ManagedError extends Error implements IDisposable { + constructor(message: string); + get stack(): string | undefined; + dispose(): void; + get isDisposed(): boolean; + toString(): string; +} +declare enum MemoryViewType { + Byte = 0, + Int32 = 1, + Double = 2 +} +interface IMemoryView { + /** + * copies elements from provided source to the wasm memory. + * target has to have the elements of the same type as the underlying C# array. + * same as TypedArray.set() + */ + set(source: TypedArray, targetOffset?: number): void; + /** + * copies elements from wasm memory to provided target. + * target has to have the elements of the same type as the underlying C# array. + */ + copyTo(target: TypedArray, sourceOffset?: number): void; + /** + * same as TypedArray.slice() + */ + slice(start?: number, end?: number): TypedArray; + get length(): number; + get byteLength(): number; +} + +declare function mono_wasm_get_assembly_exports(assembly: string): Promise<any>; + declare const MONO: { mono_wasm_setenv: typeof mono_wasm_setenv; mono_wasm_load_bytes_into_heap: typeof mono_wasm_load_bytes_into_heap; @@ -390,6 +433,7 @@ declare const MONO: { mono_wasm_release_roots: typeof mono_wasm_release_roots; mono_run_main: typeof mono_run_main; mono_run_main_and_exit: typeof mono_run_main_and_exit; + mono_wasm_get_assembly_exports: typeof mono_wasm_get_assembly_exports; mono_wasm_add_assembly: (name: string, data: VoidPtr, size: number) => number; mono_wasm_load_runtime: (unused: string, debug_level: number) => void; config: MonoConfig | MonoConfigError; @@ -490,4 +534,33 @@ declare global { function getDotnetRuntime(runtimeId: number): DotnetPublicAPI | undefined; } -export { BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, MONOType, MonoArray, MonoObject, MonoString, VoidPtr, createDotnetRuntime as default }; +/** + * Span class is JS wrapper for System.Span<T>. This view doesn't own the memory, nor pin the underlying array. + * It's ideal to be used on call from C# with the buffer pinned there or with unmanaged memory. + * It is disposed at the end of the call to JS. + */ +declare class Span implements IMemoryView, IDisposable { + dispose(): void; + get isDisposed(): boolean; + set(source: TypedArray, targetOffset?: number | undefined): void; + copyTo(target: TypedArray, sourceOffset?: number | undefined): void; + slice(start?: number | undefined, end?: number | undefined): TypedArray; + get length(): number; + get byteLength(): number; +} +/** + * ArraySegment class is JS wrapper for System.ArraySegment<T>. + * This wrapper would also pin the underlying array and hold GCHandleType.Pinned until this JS instance is collected. + * User could dispose it manualy. + */ +declare class ArraySegment implements IMemoryView, IDisposable { + dispose(): void; + get isDisposed(): boolean; + set(source: TypedArray, targetOffset?: number | undefined): void; + copyTo(target: TypedArray, sourceOffset?: number | undefined): void; + slice(start?: number | undefined, end?: number | undefined): TypedArray; + get length(): number; + get byteLength(): number; +} + +export { ArraySegment, BINDINGType, CreateDotnetRuntimeType, DotnetModuleConfig, DotnetPublicAPI, EmscriptenModule, IMemoryView, MONOType, ManagedError, ManagedObject, MemoryViewType, MonoArray, MonoObject, MonoString, Span, VoidPtr, createDotnetRuntime as default }; diff --git a/src/mono/wasm/runtime/driver.c b/src/mono/wasm/runtime/driver.c index fb0b15101fd..c8ea3e445d5 100644 --- a/src/mono/wasm/runtime/driver.c +++ b/src/mono/wasm/runtime/driver.c @@ -447,7 +447,6 @@ get_native_to_interp (MonoMethod *method, void *extra_arg) void mono_initialize_internals () { mono_add_internal_call ("Interop/Runtime::InvokeJS", mono_wasm_invoke_js); - // TODO: what happens when two types in different assemblies have the same FQN? // Blazor specific custom routines - see dotnet_support.js for backing code mono_add_internal_call ("WebAssembly.JSInterop.InternalCalls::InvokeJS", mono_wasm_invoke_js_blazor); @@ -655,6 +654,22 @@ mono_wasm_assembly_find_class (MonoAssembly *assembly, const char *namespace, co return result; } +extern int mono_runtime_run_module_cctor (MonoImage *image, MonoError *error); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_runtime_run_module_cctor (MonoAssembly *assembly) +{ + assert (assembly); + MonoError error; + MONO_ENTER_GC_UNSAFE; + MonoImage *image = mono_assembly_get_image (assembly); + if (!mono_runtime_run_module_cctor(image, &error)) { + //g_print ("Failed to run module constructor due to %s\n", mono_error_get_message (error)); + } + MONO_EXIT_GC_UNSAFE; +} + + EMSCRIPTEN_KEEPALIVE MonoMethod* mono_wasm_assembly_find_method (MonoClass *klass, const char *name, int arguments) { @@ -742,6 +757,25 @@ mono_wasm_invoke_method (MonoMethod *method, MonoObject *this_arg, void *params[ return result; } +EMSCRIPTEN_KEEPALIVE MonoObject* +mono_wasm_invoke_method_bound (MonoMethod *method, void* args)// JSMarshalerArguments +{ + MonoObject *exc = NULL; + MonoObject *res; + + void *invoke_args[1] = { args }; + + mono_runtime_invoke (method, NULL, invoke_args, &exc); + if (exc) { + MonoObject *exc2 = NULL; + res = (MonoObject*)mono_object_to_string (exc, &exc2); + if (exc2) + res = (MonoObject*) mono_string_new (root_domain, "Exception Double Fault"); + return res; + } + return NULL; +} + EMSCRIPTEN_KEEPALIVE MonoMethod* mono_wasm_assembly_get_entry_point (MonoAssembly *assembly, int auto_insert_breakpoint) { diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 19fd04fecd5..36e8a8162ae 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -111,13 +111,11 @@ const linked_functions = [ "mono_wasm_typed_array_copy_to_ref", "mono_wasm_typed_array_from_ref", "mono_wasm_typed_array_copy_from_ref", - "mono_wasm_cancel_promise_ref", - "mono_wasm_web_socket_open_ref", - "mono_wasm_web_socket_send", - "mono_wasm_web_socket_receive", - "mono_wasm_web_socket_close_ref", - "mono_wasm_web_socket_abort", "mono_wasm_compile_function_ref", + "mono_wasm_bind_js_function", + "mono_wasm_invoke_bound_function", + "mono_wasm_bind_cs_function", + "mono_wasm_marshal_promise", // pal_icushim_static.c "mono_wasm_load_icu_data", diff --git a/src/mono/wasm/runtime/export-types.ts b/src/mono/wasm/runtime/export-types.ts index 3cb8c0df090..057ed837709 100644 --- a/src/mono/wasm/runtime/export-types.ts +++ b/src/mono/wasm/runtime/export-types.ts @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. import { BINDINGType, DotnetPublicAPI, MONOType } from "./exports"; +import { IDisposable, IMemoryView, ManagedError, ManagedObject, MemoryViewType } from "./marshal"; import { DotnetModuleConfig, MonoArray, MonoObject, MonoString } from "./types"; -import { EmscriptenModule, VoidPtr } from "./types/emscripten"; +import { EmscriptenModule, TypedArray, VoidPtr } from "./types/emscripten"; // ----------------------------------------------------------- // this files has all public exports from the dotnet.js module @@ -20,10 +21,41 @@ declare global { export default createDotnetRuntime; +/** + * Span class is JS wrapper for System.Span<T>. This view doesn't own the memory, nor pin the underlying array. + * It's ideal to be used on call from C# with the buffer pinned there or with unmanaged memory. + * It is disposed at the end of the call to JS. + */ +declare class Span implements IMemoryView, IDisposable { + dispose(): void; + get isDisposed(): boolean; + set(source: TypedArray, targetOffset?: number | undefined): void; + copyTo(target: TypedArray, sourceOffset?: number | undefined): void; + slice(start?: number | undefined, end?: number | undefined): TypedArray; + get length(): number; + get byteLength(): number; +} + +/** + * ArraySegment class is JS wrapper for System.ArraySegment<T>. + * This wrapper would also pin the underlying array and hold GCHandleType.Pinned until this JS instance is collected. + * User could dispose it manualy. + */ +declare class ArraySegment implements IMemoryView, IDisposable { + dispose(): void; + get isDisposed(): boolean; + set(source: TypedArray, targetOffset?: number | undefined): void; + copyTo(target: TypedArray, sourceOffset?: number | undefined): void; + slice(start?: number | undefined, end?: number | undefined): TypedArray; + get length(): number; + get byteLength(): number; +} + export { VoidPtr, MonoObject, MonoString, MonoArray, BINDINGType, MONOType, EmscriptenModule, - DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType + DotnetPublicAPI, DotnetModuleConfig, CreateDotnetRuntimeType, + IMemoryView, MemoryViewType, ManagedObject, ManagedError, Span, ArraySegment }; diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index 34da483b5ec..34646ca213b 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -67,7 +67,13 @@ import { create_weak_ref } from "./weak-ref"; import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; import { mono_run_main, mono_run_main_and_exit } from "./run"; +import { dynamic_import, get_global_this, get_property, get_typeof_property, has_property, mono_wasm_bind_js_function, mono_wasm_invoke_bound_function, set_property } from "./invoke-js"; +import { mono_wasm_bind_cs_function, mono_wasm_get_assembly_exports } from "./invoke-cs"; +import { mono_wasm_marshal_promise } from "./marshal-to-js"; +import { ws_wasm_abort, ws_wasm_close, ws_wasm_create, ws_wasm_open, ws_wasm_receive, ws_wasm_send } from "./web-socket"; +import { http_wasm_abort_request, http_wasm_abort_response, http_wasm_create_abort_controler, http_wasm_fetch, http_wasm_fetch_bytes, http_wasm_get_response_bytes, http_wasm_get_response_header_names, http_wasm_get_response_header_values, http_wasm_get_response_length, http_wasm_get_streamed_response_bytes, http_wasm_supports_streaming_response } from "./http"; import { diagnostics } from "./diagnostics"; +import { mono_wasm_cancel_promise } from "./cancelable-promise"; import { dotnet_browser_can_use_subtle_crypto_impl, dotnet_browser_simple_digest_hash, @@ -75,8 +81,6 @@ import { dotnet_browser_encrypt_decrypt, dotnet_browser_derive_bits, } from "./crypto-worker"; -import { mono_wasm_cancel_promise_ref } from "./cancelable-promise"; -import { mono_wasm_web_socket_open_ref, mono_wasm_web_socket_send, mono_wasm_web_socket_receive, mono_wasm_web_socket_close_ref, mono_wasm_web_socket_abort } from "./web-socket"; import { mono_wasm_pthread_on_pthread_attached, afterThreadInitTLS } from "./pthreads/worker"; import { afterLoadWasmModuleToWorker } from "./pthreads/browser"; @@ -95,6 +99,7 @@ const MONO = { mono_wasm_release_roots, mono_run_main, mono_run_main_and_exit, + mono_wasm_get_assembly_exports, // for Blazor's future! mono_wasm_add_assembly: cwraps.mono_wasm_add_assembly, @@ -393,12 +398,10 @@ export const __linker_exports: any = { mono_wasm_typed_array_copy_to_ref, mono_wasm_typed_array_from_ref, mono_wasm_typed_array_copy_from_ref, - mono_wasm_cancel_promise_ref, - mono_wasm_web_socket_open_ref, - mono_wasm_web_socket_send, - mono_wasm_web_socket_receive, - mono_wasm_web_socket_close_ref, - mono_wasm_web_socket_abort, + mono_wasm_bind_js_function, + mono_wasm_invoke_bound_function, + mono_wasm_bind_cs_function, + mono_wasm_marshal_promise, // also keep in sync with pal_icushim_static.c mono_wasm_load_icu_data, @@ -451,6 +454,37 @@ const INTERNAL: any = { mono_wasm_change_debugger_log_level, mono_wasm_debugger_attached, mono_wasm_runtime_is_ready: <boolean>runtimeHelpers.mono_wasm_runtime_is_ready, + + // interop + get_property, + set_property, + has_property, + get_typeof_property, + get_global_this, + get_dotnet_instance, + dynamic_import, + + // BrowserWebSocket + mono_wasm_cancel_promise, + ws_wasm_create, + ws_wasm_open, + ws_wasm_send, + ws_wasm_receive, + ws_wasm_close, + ws_wasm_abort, + + // BrowserHttpHandler + http_wasm_supports_streaming_response, + http_wasm_create_abort_controler, + http_wasm_abort_request, + http_wasm_abort_response, + http_wasm_fetch, + http_wasm_fetch_bytes, + http_wasm_get_response_header_names, + http_wasm_get_response_header_values, + http_wasm_get_response_bytes, + http_wasm_get_response_length, + http_wasm_get_streamed_response_bytes, }; // this represents visibility in the javascript diff --git a/src/mono/wasm/runtime/gc-common.h b/src/mono/wasm/runtime/gc-common.h index 4ea2bb06797..92789e641a3 100644 --- a/src/mono/wasm/runtime/gc-common.h +++ b/src/mono/wasm/runtime/gc-common.h @@ -61,3 +61,9 @@ static void copy_volatile (PPVOLATILE(MonoObject) destination, PPVOLATILE(MonoObject) source) { mono_gc_wbarrier_generic_store_atomic((void*)destination, (MonoObject*)(*source)); } + +EMSCRIPTEN_KEEPALIVE int +mono_wasm_register_root (char *start, size_t size, const char *name); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_deregister_root (char *addr);
\ No newline at end of file diff --git a/src/mono/wasm/runtime/http.ts b/src/mono/wasm/runtime/http.ts new file mode 100644 index 00000000000..d8249fad0b9 --- /dev/null +++ b/src/mono/wasm/runtime/http.ts @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { wrap_as_cancelable_promise } from "./cancelable-promise"; +import { MemoryViewType, Span } from "./marshal"; +import { mono_assert } from "./types"; +import { VoidPtr } from "./types/emscripten"; + +export function http_wasm_supports_streaming_response(): boolean { + return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function"; +} + +export function http_wasm_create_abort_controler(): AbortController { + return new AbortController(); +} + +export function http_wasm_abort_request(abort_controller: AbortController): void { + abort_controller.abort(); +} + +export function http_wasm_abort_response(res: ResponseExtension): void { + res.__abort_controller.abort(); + if (res.__reader) { + res.__reader.cancel(); + } +} + +export function http_wasm_fetch_bytes(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, bodyPtr: VoidPtr, bodyLength: number): Promise<ResponseExtension> { + // the bufferPtr is pinned by the caller + const view = new Span(bodyPtr, bodyLength, MemoryViewType.Byte); + const copy = view.slice() as Uint8Array; + return http_wasm_fetch(url, header_names, header_values, option_names, option_values, abort_controller, copy); +} + +export function http_wasm_fetch(url: string, header_names: string[], header_values: string[], option_names: string[], option_values: any[], abort_controller: AbortController, body: string | Uint8Array | null): Promise<ResponseExtension> { + mono_assert(url && typeof url === "string", "expected url string"); + mono_assert(header_names && header_values && Array.isArray(header_names) && Array.isArray(header_values) && header_names.length === header_values.length, "expected headerNames and headerValues arrays"); + mono_assert(option_names && option_values && Array.isArray(option_names) && Array.isArray(option_values) && option_names.length === option_values.length, "expected headerNames and headerValues arrays"); + const headers = new Headers(); + for (let i = 0; i < header_names.length; i++) { + headers.append(header_names[i], header_values[i]); + } + const options: any = { + body, + headers, + signal: abort_controller.signal + }; + for (let i = 0; i < option_names.length; i++) { + options[option_names[i]] = option_values[i]; + } + + return wrap_as_cancelable_promise(async () => { + const res = await fetch(url, options) as ResponseExtension; + res.__abort_controller = abort_controller; + return res; + }); +} + +function get_response_headers(res: ResponseExtension): void { + if (!res.__headerNames) { + res.__headerNames = []; + res.__headerValues = []; + const entries: Iterable<string[]> = (<any>res.headers).entries(); + + for (const pair of entries) { + res.__headerNames.push(pair[0]); + res.__headerValues.push(pair[1]); + } + } +} + +export function http_wasm_get_response_header_names(res: ResponseExtension): string[] { + get_response_headers(res); + return res.__headerNames; +} + +export function http_wasm_get_response_header_values(res: ResponseExtension): string[] { + get_response_headers(res); + return res.__headerValues; +} + +export function http_wasm_get_response_length(res: ResponseExtension): Promise<number> { + return wrap_as_cancelable_promise(async () => { + const buffer = await res.arrayBuffer(); + res.__buffer = buffer; + res.__source_offset = 0; + return buffer.byteLength; + }); +} + +export function http_wasm_get_response_bytes(res: ResponseExtension, view: Span): number { + mono_assert(res.__buffer, "expected resoved arrayBuffer"); + if (res.__source_offset == res.__buffer!.byteLength) { + return 0; + } + const source_view = new Uint8Array(res.__buffer!, res.__source_offset); + view.set(source_view, 0); + const bytes_read = Math.min(view.byteLength, source_view.byteLength); + res.__source_offset += bytes_read; + return bytes_read; +} + +export async function http_wasm_get_streamed_response_bytes(res: ResponseExtension, bufferPtr: VoidPtr, bufferLength: number): Promise<number> { + // the bufferPtr is pinned by the caller + const view = new Span(bufferPtr, bufferLength, MemoryViewType.Byte); + return wrap_as_cancelable_promise(async () => { + if (!res.__chunk && res.body) { + res.__reader = res.body.getReader(); + res.__chunk = await res.__reader.read(); + res.__source_offset = 0; + } + + let target_offset = 0; + let bytes_read = 0; + // loop until end of browser stream or end of C# buffer + while (res.__reader && res.__chunk && !res.__chunk.done) { + const remaining_source = res.__chunk.value.byteLength - res.__source_offset; + if (remaining_source === 0) { + res.__chunk = await res.__reader.read(); + res.__source_offset = 0; + continue;// are we done yet + } + + const remaining_target = view.byteLength - target_offset; + const bytes_copied = Math.min(remaining_source, remaining_target); + const source_view = res.__chunk.value.subarray(res.__source_offset, res.__source_offset + bytes_copied); + + // copy available bytes + view.set(source_view, target_offset); + target_offset += bytes_copied; + bytes_read += bytes_copied; + res.__source_offset += bytes_copied; + + if (target_offset == view.byteLength) { + return bytes_read; + } + } + return bytes_read; + }); +} + +interface ResponseExtension extends Response { + __buffer?: ArrayBuffer + __reader?: ReadableStreamDefaultReader<Uint8Array> + __chunk?: ReadableStreamDefaultReadResult<Uint8Array> + __source_offset: number + __abort_controller: AbortController + __headerNames: string[]; + __headerValues: string[]; +}
\ No newline at end of file diff --git a/src/mono/wasm/runtime/invoke-cs.ts b/src/mono/wasm/runtime/invoke-cs.ts new file mode 100644 index 00000000000..0201be0d2d9 --- /dev/null +++ b/src/mono/wasm/runtime/invoke-cs.ts @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { MonoObject, MonoString } from "./export-types"; +import { EXPORTS, Module, runtimeHelpers } from "./imports"; +import { generate_arg_marshal_to_cs } from "./marshal-to-cs"; +import { marshal_exception_to_js, generate_arg_marshal_to_js } from "./marshal-to-js"; +import { + JSMarshalerArguments, JavaScriptMarshalerArgSize, JSFunctionSignature, + JSMarshalerTypeSize, JSMarshalerSignatureHeaderSize, + get_arg, get_sig, set_arg_type, + get_signature_argument_count, is_args_exception, bound_cs_function_symbol, get_signature_version, MarshalerType, +} from "./marshal"; +import { parseFQN, wrap_error_root } from "./method-calls"; +import { mono_wasm_new_external_root, mono_wasm_new_root } from "./roots"; +import { conv_string, conv_string_root } from "./strings"; +import { mono_assert, MonoObjectRef, MonoStringRef } from "./types"; +import { Int32Ptr } from "./types/emscripten"; +import cwraps, { wrap_c_function } from "./cwraps"; +import { find_method } from "./method-binding"; +import { assembly_load } from "./class-loader"; + +const exportedMethods = new Map<string, Function>(); + +export function mono_wasm_bind_cs_function(fully_qualified_name: MonoStringRef, signature_hash: number, signature: JSFunctionSignature, is_exception: Int32Ptr, result_address: MonoObjectRef): void { + const fqn_root = mono_wasm_new_external_root<MonoString>(fully_qualified_name), resultRoot = mono_wasm_new_external_root<MonoObject>(result_address); + const anyModule = Module as any; + try { + const version = get_signature_version(signature); + mono_assert(version === 1, () => `Signature version ${version} mismatch.`); + + const args_count = get_signature_argument_count(signature); + const js_fqn = conv_string_root(fqn_root)!; + mono_assert(js_fqn, "fully_qualified_name must be string"); + + if (runtimeHelpers.config.diagnostic_tracing) { + console.trace(`MONO_WASM: Binding [JSExport] ${js_fqn}`); + } + + const { assembly, namespace, classname, methodname } = parseFQN(js_fqn); + + const asm = assembly_load(assembly); + if (!asm) + throw new Error("Could not find assembly: " + assembly); + + const klass = cwraps.mono_wasm_assembly_find_class(asm, namespace, classname); + if (!klass) + throw new Error("Could not find class: " + namespace + ":" + classname + " in assembly " + assembly); + + const wrapper_name = `__Wrapper_${methodname}_${signature_hash}`; + const method = find_method(klass, wrapper_name, -1); + if (!method) + throw new Error(`Could not find method: ${wrapper_name} in ${klass} [${assembly}]`); + + const closure: any = { + method, get_arg, signature, + stackSave: anyModule.stackSave, stackAlloc: anyModule.stackAlloc, stackRestore: anyModule.stackRestore, + conv_string, + mono_wasm_new_root, init_void, init_result, /*init_argument,*/ marshal_exception_to_js, is_args_exception, + mono_wasm_invoke_method_bound: wrap_c_function("mono_wasm_invoke_method_bound"), + }; + const bound_js_function_name = "_bound_cs_" + `${namespace}_${classname}_${methodname}`.replace(/\./g, "_"); + let body = `//# sourceURL=https://mono-wasm.invalid/${bound_js_function_name} \n`; + let bodyToCs = ""; + let converter_names = ""; + + for (let index = 0; index < args_count; index++) { + const arg_offset = (index + 2) * JavaScriptMarshalerArgSize; + const sig_offset = (index + 2) * JSMarshalerTypeSize + JSMarshalerSignatureHeaderSize; + const sig = get_sig(signature, index + 2); + const { converters, call_body } = generate_arg_marshal_to_cs(sig, index + 2, arg_offset, sig_offset, `arguments[${index}]`, closure); + converter_names += converters; + bodyToCs += call_body; + } + const { converters: res_converters, call_body: res_call_body, marshaler_type: res_marshaler_type } = generate_arg_marshal_to_js(get_sig(signature, 1), 1, JavaScriptMarshalerArgSize, JSMarshalerTypeSize + JSMarshalerSignatureHeaderSize, "js_result", closure); + converter_names += res_converters; + + body += `const { method, get_arg, signature, stackSave, stackAlloc, stackRestore, mono_wasm_new_root, conv_string, init_void, init_result, init_argument, marshal_exception_to_js, is_args_exception, mono_wasm_invoke_method_bound ${converter_names} } = closure;\n`; + // TODO named arguments instead of arguments keyword + body += `return function ${bound_js_function_name} () {\n`; + if (res_marshaler_type === MarshalerType.String) { + body += "let root = null;\n"; + } + body += "const sp = stackSave();\n"; + body += "try {\n"; + body += ` const args = stackAlloc(${(args_count + 2) * JavaScriptMarshalerArgSize});\n`; + if (res_marshaler_type !== MarshalerType.Void && res_marshaler_type !== MarshalerType.Discard) { + if (res_marshaler_type === MarshalerType.String) { + body += " root = mono_wasm_new_root(0);\n"; + body += " init_result(args);\n"; + } + else { + body += " init_result(args);\n"; + } + } else { + body += " init_void(args);\n"; + } + + body += bodyToCs; + + body += " const fail = mono_wasm_invoke_method_bound(method, args);\n"; + body += " if (fail) throw new Error(\"ERR22: Unexpected error: \" + conv_string(fail));\n"; + body += " if (is_args_exception(args)) throw marshal_exception_to_js(get_arg(args, 0));\n"; + if (res_marshaler_type !== MarshalerType.Void && res_marshaler_type !== MarshalerType.Discard) { + body += res_call_body; + } + + if (res_marshaler_type !== MarshalerType.Void && res_marshaler_type !== MarshalerType.Discard) { + body += "return js_result;\n"; + } + + body += "} finally {\n"; + body += " stackRestore(sp);\n"; + if (res_marshaler_type === MarshalerType.String) { + body += " if(root) root.release()\n"; + } + body += "}}"; + const factory = new Function("closure", body); + const bound_fn = factory(closure); + bound_fn[bound_cs_function_symbol] = true; + + exportedMethods.set(js_fqn, bound_fn); + _walk_exports_to_set_function(assembly, namespace, classname, methodname, signature_hash, bound_fn); + } + catch (ex) { + wrap_error_root(is_exception, ex, resultRoot); + } finally { + resultRoot.release(); + fqn_root.release(); + } +} + +function init_void(args: JSMarshalerArguments) { + mono_assert(args && (<any>args) % 8 == 0, "Arg alignment"); + const exc = get_arg(args, 0); + set_arg_type(exc, MarshalerType.None); + + const res = get_arg(args, 1); + set_arg_type(res, MarshalerType.None); +} + +function init_result(args: JSMarshalerArguments) { + mono_assert(args && (<any>args) % 8 == 0, "Arg alignment"); + const exc = get_arg(args, 0); + set_arg_type(exc, MarshalerType.None); + + const res = get_arg(args, 1); + set_arg_type(res, MarshalerType.None); +} + +export const exportsByAssembly: Map<string, any> = new Map(); + +function _walk_exports_to_set_function(assembly: string, namespace: string, classname: string, methodname: string, signature_hash: number, fn: Function): void { + let scope: any = EXPORTS; + let parts = `${namespace}.${classname}`.split("."); + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + let newscope = scope[part]; + if (!newscope) { + newscope = {}; + scope[part] = newscope; + } + mono_assert(newscope, () => `${part} not found while looking up ${classname}`); + scope = newscope; + } + + if (!scope[methodname]) { + scope[methodname] = fn; + } + scope[`${methodname}.${signature_hash}`] = fn; + + // do it again for per assemly scope + let assemblyScope = exportsByAssembly.get(assembly); + if (!assemblyScope) { + assemblyScope = {}; + exportsByAssembly.set(assembly, assemblyScope); + exportsByAssembly.set(assembly + ".dll", assemblyScope); + } + scope = assemblyScope; + parts = `${namespace}.${classname}`.split("."); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + let newscope = scope[part]; + if (!newscope) { + newscope = {}; + scope[part] = newscope; + } + mono_assert(newscope, () => `${part} not found while looking up ${classname}`); + scope = newscope; + } + + if (!scope[methodname]) { + scope[methodname] = fn; + } + scope[`${methodname}.${signature_hash}`] = fn; +} + +export async function mono_wasm_get_assembly_exports(assembly: string): Promise<any> { + const asm = assembly_load(assembly); + if (!asm) + throw new Error("Could not find assembly: " + assembly); + cwraps.mono_wasm_runtime_run_module_cctor(asm); + + return exportsByAssembly.get(assembly) || {}; +}
\ No newline at end of file diff --git a/src/mono/wasm/runtime/invoke-js.ts b/src/mono/wasm/runtime/invoke-js.ts new file mode 100644 index 00000000000..0f6b64010b3 --- /dev/null +++ b/src/mono/wasm/runtime/invoke-js.ts @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle } from "./gc-handles"; +import { marshal_exception_to_cs, generate_arg_marshal_to_cs } from "./marshal-to-cs"; +import { get_signature_argument_count, JSMarshalerArguments as JSMarshalerArguments, JavaScriptMarshalerArgSize, JSFunctionSignature as JSFunctionSignature, bound_js_function_symbol, JSMarshalerTypeSize, get_sig, JSMarshalerSignatureHeaderSize, get_signature_version, MarshalerType, get_signature_type } from "./marshal"; +import { setI32 } from "./memory"; +import { wrap_error_root } from "./method-calls"; +import { conv_string_root } from "./strings"; +import { mono_assert, JSHandle, MonoObject, MonoObjectRef, MonoString, MonoStringRef } from "./types"; +import { Int32Ptr } from "./types/emscripten"; +import { IMPORTS, INTERNAL, runtimeHelpers } from "./imports"; +import { generate_arg_marshal_to_js } from "./marshal-to-js"; +import { mono_wasm_new_external_root } from "./roots"; + +export function mono_wasm_bind_js_function(function_name: MonoStringRef, module_name: MonoStringRef, signature: JSFunctionSignature, function_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { + const function_name_root = mono_wasm_new_external_root<MonoString>(function_name), + module_name_root = mono_wasm_new_external_root<MonoString>(module_name), + resultRoot = mono_wasm_new_external_root<MonoObject>(result_address); + try { + const version = get_signature_version(signature); + mono_assert(version === 1, () => `Signature version ${version} mismatch.`); + + const js_function_name = conv_string_root(function_name_root)!; + const js_module_name = conv_string_root(module_name_root)!; + if (runtimeHelpers.config.diagnostic_tracing) { + console.trace(`MONO_WASM: Binding [JSImport] ${js_function_name} from ${js_module_name}`); + } + const fn = mono_wasm_lookup_function(js_function_name, js_module_name); + const args_count = get_signature_argument_count(signature); + + const closure: any = { fn, marshal_exception_to_cs, signature }; + const bound_js_function_name = "_bound_js_" + js_function_name.replace(/\./g, "_"); + let body = `//# sourceURL=https://mono-wasm.invalid/${bound_js_function_name} \n`; + let converter_names = ""; + + + let bodyToJs = ""; + let pass_args = ""; + for (let index = 0; index < args_count; index++) { + const arg_offset = (index + 2) * JavaScriptMarshalerArgSize; + const sig_offset = (index + 2) * JSMarshalerTypeSize + JSMarshalerSignatureHeaderSize; + const arg_name = `arg${index}`; + const sig = get_sig(signature, index + 2); + const { converters, call_body } = generate_arg_marshal_to_js(sig, index + 2, arg_offset, sig_offset, arg_name, closure); + converter_names += converters; + bodyToJs += call_body; + if (pass_args === "") { + pass_args += arg_name; + } else { + pass_args += `, ${arg_name}`; + } + } + const { converters: res_converters, call_body: res_call_body, marshaler_type: res_marshaler_type } = generate_arg_marshal_to_cs(get_sig(signature, 1), 1, JavaScriptMarshalerArgSize, JSMarshalerTypeSize + JSMarshalerSignatureHeaderSize, "js_result", closure); + converter_names += res_converters; + + body += `const { signature, fn, marshal_exception_to_cs ${converter_names} } = closure;\n`; + body += `return function ${bound_js_function_name} (args) { try {\n`; + // body += `console.log("${bound_js_function_name}")\n`; + body += bodyToJs; + + + if (res_marshaler_type === MarshalerType.Void) { + body += ` const js_result = fn(${pass_args});\n`; + body += ` if (js_result !== undefined) throw new Error('Function ${js_function_name} returned unexpected value, C# signature is void');\n`; + } + else if (res_marshaler_type === MarshalerType.Discard) { + body += ` fn(${pass_args});\n`; + } + else { + body += ` const js_result = fn(${pass_args});\n`; + body += res_call_body; + } + + for (let index = 0; index < args_count; index++) { + const sig = get_sig(signature, index + 2); + const marshaler_type = get_signature_type(sig); + if (marshaler_type == MarshalerType.Span) { + const arg_name = `arg${index}`; + body += ` ${arg_name}.dispose();\n`; + } + } + + body += "} catch (ex) {\n"; + body += " marshal_exception_to_cs(args, ex);\n"; + body += "}}"; + const factory = new Function("closure", body); + const bound_fn = factory(closure); + bound_fn[bound_js_function_symbol] = true; + const bound_function_handle = mono_wasm_get_js_handle(bound_fn)!; + setI32(function_js_handle, <any>bound_function_handle); + } catch (ex) { + wrap_error_root(is_exception, ex, resultRoot); + } finally { + resultRoot.release(); + function_name_root.release(); + } +} + +export function mono_wasm_invoke_bound_function(bound_function_js_handle: JSHandle, args: JSMarshalerArguments): void { + const bound_fn = mono_wasm_get_jsobj_from_js_handle(bound_function_js_handle); + mono_assert(bound_fn && typeof (bound_fn) === "function" && bound_fn[bound_js_function_symbol], () => `Bound function handle expected ${bound_function_js_handle}`); + bound_fn(args); +} + +function mono_wasm_lookup_function(function_name: string, js_module_name: string): Function { + mono_assert(function_name && typeof function_name === "string", "function_name must be string"); + + let scope: any = IMPORTS; + const parts = function_name.split("."); + if (js_module_name) { + scope = importedModules.get(js_module_name); + mono_assert(scope, () => `ES6 module ${js_module_name} was not imported yet, please call JSHost.Import() first.`); + } + else if (parts[0] === "INTERNAL") { + scope = INTERNAL; + parts.shift(); + } + else if (parts[0] === "globalThis") { + scope = globalThis; + parts.shift(); + } + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + const newscope = scope[part]; + mono_assert(newscope, () => `${part} not found while looking up ${function_name}`); + scope = newscope; + } + + const fname = parts[parts.length - 1]; + const fn = scope[fname]; + + mono_assert(typeof (fn) === "function", () => `${function_name} must be a Function but was ${typeof fn}`); + return fn; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function set_property(self: any, name: string, value: any): void { + mono_assert(self, "Null reference"); + self[name] = value; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function get_property(self: any, name: string): any { + mono_assert(self, "Null reference"); + return self[name]; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function has_property(self: any, name: string): boolean { + mono_assert(self, "Null reference"); + return name in self; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function get_typeof_property(self: any, name: string): string { + mono_assert(self, "Null reference"); + return typeof self[name]; +} + +export function get_global_this(): any { + return globalThis; +} + +export const importedModulesPromises: Map<string, Promise<any>> = new Map(); +export const importedModules: Map<string, Promise<any>> = new Map(); + +export async function dynamic_import(module_name: string, module_url: string): Promise<any> { + mono_assert(module_name, "Invalid module_name"); + mono_assert(module_url, "Invalid module_name"); + let promise = importedModulesPromises.get(module_name); + const newPromise = !promise; + if (newPromise) { + if (runtimeHelpers.config.diagnostic_tracing) + console.trace(`MONO_WASM: importing ES6 module '${module_name}' from '${module_url}'`); + promise = import(module_url); + importedModulesPromises.set(module_name, promise); + } + const module = await promise; + if (newPromise) { + importedModules.set(module_name, module); + if (runtimeHelpers.config.diagnostic_tracing) + console.trace(`MONO_WASM: imported ES6 module '${module_name}' from '${module_url}'`); + } + return module; +} + + diff --git a/src/mono/wasm/runtime/marshal-to-cs.ts b/src/mono/wasm/runtime/marshal-to-cs.ts new file mode 100644 index 00000000000..fff46ba6043 --- /dev/null +++ b/src/mono/wasm/runtime/marshal-to-cs.ts @@ -0,0 +1,651 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { isThenable } from "./cancelable-promise"; +import wrapped_cs_functions from "./corebindings"; +import cwraps from "./cwraps"; +import { assert_not_disposed, cs_owned_js_handle_symbol, js_owned_gc_handle_symbol, mono_wasm_get_js_handle, setup_managed_proxy, teardown_managed_proxy } from "./gc-handles"; +import { Module, runtimeHelpers } from "./imports"; +import { + JSMarshalerArgument, ManagedError, + set_gc_handle, set_js_handle, set_arg_type, set_arg_i32, set_arg_f64, set_arg_i52, set_arg_f32, set_arg_i16, set_arg_u8, set_arg_b8, set_arg_date, + set_arg_length, get_arg, is_args_exception, JavaScriptMarshalerArgSize, get_signature_type, get_signature_arg1_type, get_signature_arg2_type, cs_to_js_marshalers, js_to_cs_marshalers, + MarshalerToCs, MarshalerToJs, get_signature_res_type, JSMarshalerArguments, bound_js_function_symbol, set_arg_u16, JSMarshalerType, array_element_size, get_string_root, Span, ArraySegment, MemoryViewType, get_signature_arg3_type, MarshalerType, set_arg_i64_big, set_arg_intptr, IDisposable, set_arg_element_type, ManagedObject +} from "./marshal"; +import { marshal_exception_to_js } from "./marshal-to-js"; +import { _zero_region } from "./memory"; +import { conv_string, js_string_to_mono_string_root } from "./strings"; +import { mono_assert, GCHandle, GCHandleNull } from "./types"; +import { TypedArray } from "./types/emscripten"; + +export function initialize_marshalers_to_cs(): void { + if (js_to_cs_marshalers.size == 0) { + js_to_cs_marshalers.set(MarshalerType.Array, _marshal_array_to_cs); + js_to_cs_marshalers.set(MarshalerType.Span, _marshal_span_to_cs); + js_to_cs_marshalers.set(MarshalerType.ArraySegment, _marshal_array_segment_to_cs); + js_to_cs_marshalers.set(MarshalerType.Boolean, _marshal_bool_to_cs); + js_to_cs_marshalers.set(MarshalerType.Byte, _marshal_byte_to_cs); + js_to_cs_marshalers.set(MarshalerType.Char, _marshal_char_to_cs); + js_to_cs_marshalers.set(MarshalerType.Int16, _marshal_int16_to_cs); + js_to_cs_marshalers.set(MarshalerType.Int32, _marshal_int32_to_cs); + js_to_cs_marshalers.set(MarshalerType.Int52, _marshal_int52_to_cs); + js_to_cs_marshalers.set(MarshalerType.BigInt64, _marshal_bigint64_to_cs); + js_to_cs_marshalers.set(MarshalerType.Double, _marshal_double_to_cs); + js_to_cs_marshalers.set(MarshalerType.Single, _marshal_float_to_cs); + js_to_cs_marshalers.set(MarshalerType.IntPtr, _marshal_intptr_to_cs); + js_to_cs_marshalers.set(MarshalerType.DateTime, _marshal_date_time_to_cs); + js_to_cs_marshalers.set(MarshalerType.DateTimeOffset, _marshal_date_time_offset_to_cs); + js_to_cs_marshalers.set(MarshalerType.String, _marshal_string_to_cs); + js_to_cs_marshalers.set(MarshalerType.Exception, marshal_exception_to_cs); + js_to_cs_marshalers.set(MarshalerType.JSException, marshal_exception_to_cs); + js_to_cs_marshalers.set(MarshalerType.JSObject, _marshal_js_object_to_cs); + js_to_cs_marshalers.set(MarshalerType.Object, _marshal_cs_object_to_cs); + js_to_cs_marshalers.set(MarshalerType.Task, _marshal_task_to_cs); + js_to_cs_marshalers.set(MarshalerType.Action, _marshal_function_to_cs); + js_to_cs_marshalers.set(MarshalerType.Function, _marshal_function_to_cs); + js_to_cs_marshalers.set(MarshalerType.None, _marshal_null_to_cs);// also void + js_to_cs_marshalers.set(MarshalerType.Discard, _marshal_null_to_cs);// also void + js_to_cs_marshalers.set(MarshalerType.Void, _marshal_null_to_cs);// also void + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function generate_arg_marshal_to_cs(sig: JSMarshalerType, index: number, arg_offset: number, sig_offset: number, jsname: string, closure: any): { + converters: string, + call_body: string, + marshaler_type: MarshalerType +} { + let converters = ""; + let converter_types = ""; + let call_body = ""; + const converter_name = "converter" + index; + let converter_name_arg1 = "null"; + let converter_name_arg2 = "null"; + let converter_name_arg3 = "null"; + let converter_name_res = "null"; + + let marshaler_type = get_signature_type(sig); + if (marshaler_type === MarshalerType.None || marshaler_type === MarshalerType.Void) { + return { + converters, + call_body, + marshaler_type + }; + } + + const marshaler_type_res = get_signature_res_type(sig); + if (marshaler_type_res !== MarshalerType.None) { + const converter = js_to_cs_marshalers.get(marshaler_type_res); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_res} at ${index}`); + + + if (marshaler_type != MarshalerType.Nullable) { + converter_name_res = "converter" + index + "_res"; + converters += ", " + converter_name_res; + converter_types += " " + MarshalerType[marshaler_type_res]; + closure[converter_name_res] = converter; + } + else { + marshaler_type = marshaler_type_res; + } + } + + const marshaler_type_arg1 = get_signature_arg1_type(sig); + if (marshaler_type_arg1 !== MarshalerType.None) { + const converter = cs_to_js_marshalers.get(marshaler_type_arg1); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg1} at ${index}`); + + converter_name_arg1 = "converter" + index + "_arg1"; + converters += ", " + converter_name_arg1; + converter_types += " " + MarshalerType[marshaler_type_arg1]; + closure[converter_name_arg1] = converter; + } + + const marshaler_type_arg2 = get_signature_arg2_type(sig); + if (marshaler_type_arg2 !== MarshalerType.None) { + const converter = cs_to_js_marshalers.get(marshaler_type_arg2); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg2} at ${index}`); + + converter_name_arg2 = "converter" + index + "_arg2"; + converters += ", " + converter_name_arg2; + converter_types += " " + MarshalerType[marshaler_type_arg2]; + closure[converter_name_arg2] = converter; + } + + const marshaler_type_arg3 = get_signature_arg3_type(sig); + if (marshaler_type_arg3 !== MarshalerType.None) { + const converter = cs_to_js_marshalers.get(marshaler_type_arg3); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg3} at ${index}`); + + converter_name_arg3 = "converter" + index + "_arg3"; + converters += ", " + converter_name_arg3; + converter_types += " " + MarshalerType[marshaler_type_arg3]; + closure[converter_name_arg3] = converter; + } + + const converter = js_to_cs_marshalers.get(marshaler_type); + + const arg_type_name = MarshalerType[marshaler_type]; + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${arg_type_name} (${marshaler_type}) at ${index} `); + + converters += ", " + converter_name; + converter_types += " " + arg_type_name; + closure[converter_name] = converter; + + + if (marshaler_type == MarshalerType.Task) { + call_body = ` ${converter_name}(args + ${arg_offset}, ${jsname}, signature + ${sig_offset}, ${converter_name_res}); // ${converter_types} \n`; + } else if (marshaler_type == MarshalerType.Action || marshaler_type == MarshalerType.Function) { + call_body = ` ${converter_name}(args + ${arg_offset}, ${jsname}, signature + ${sig_offset}, ${converter_name_res}, ${converter_name_arg1}, ${converter_name_arg2}, ${converter_name_arg2}); // ${converter_types} \n`; + } else { + call_body = ` ${converter_name}(args + ${arg_offset}, ${jsname}, signature + ${sig_offset}); // ${converter_types} \n`; + } + + return { + converters, + call_body, + marshaler_type + }; +} + +function _marshal_bool_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Boolean); + set_arg_b8(arg, value); + } +} + +function _marshal_byte_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Byte); + set_arg_u8(arg, value); + } +} + +function _marshal_char_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Char); + set_arg_u16(arg, value); + } +} + +function _marshal_int16_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Int16); + set_arg_i16(arg, value); + } +} + +function _marshal_int32_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Int32); + set_arg_i32(arg, value); + } +} + +function _marshal_int52_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Int52); + set_arg_i52(arg, value); + } +} + +function _marshal_bigint64_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.BigInt64); + set_arg_i64_big(arg, value); + } +} + +function _marshal_double_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Double); + set_arg_f64(arg, value); + } +} + +function _marshal_float_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.Single); + set_arg_f32(arg, value); + } +} + +function _marshal_intptr_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.IntPtr); + set_arg_intptr(arg, value); + } +} + +function _marshal_date_time_to_cs(arg: JSMarshalerArgument, value: Date): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + mono_assert(value instanceof Date, "Value is not a Date"); + set_arg_type(arg, MarshalerType.DateTime); + set_arg_date(arg, value); + } +} + +function _marshal_date_time_offset_to_cs(arg: JSMarshalerArgument, value: Date): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + mono_assert(value instanceof Date, "Value is not a Date"); + set_arg_type(arg, MarshalerType.DateTimeOffset); + set_arg_date(arg, value); + } +} + +function _marshal_string_to_cs(arg: JSMarshalerArgument, value: string) { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + set_arg_type(arg, MarshalerType.String); + mono_assert(typeof value === "string", "Value is not a String"); + _marshal_string_to_cs_impl(arg, value); + } +} + +function _marshal_string_to_cs_impl(arg: JSMarshalerArgument, value: string) { + const root = get_string_root(arg); + try { + js_string_to_mono_string_root(value, root); + } + finally { + root.release(); + } +} + +function _marshal_null_to_cs(arg: JSMarshalerArgument) { + set_arg_type(arg, MarshalerType.None); +} + +function _marshal_function_to_cs(arg: JSMarshalerArgument, value: Function, _?: JSMarshalerType, res_converter?: MarshalerToCs, arg1_converter?: MarshalerToJs, arg2_converter?: MarshalerToJs, arg3_converter?: MarshalerToJs): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + return; + } + mono_assert(value && value instanceof Function, "Value is not a Function"); + + // TODO: we could try to cache value -> exising JSHandle + const marshal_function_to_cs_wrapper: any = (args: JSMarshalerArguments) => { + const exc = get_arg(args, 0); + const res = get_arg(args, 1); + const arg1 = get_arg(args, 2); + const arg2 = get_arg(args, 3); + const arg3 = get_arg(args, 4); + + try { + let arg1_js: any = undefined; + let arg2_js: any = undefined; + let arg3_js: any = undefined; + if (arg1_converter) { + arg1_js = arg1_converter(arg1); + } + if (arg2_converter) { + arg2_js = arg2_converter(arg2); + } + if (arg3_converter) { + arg3_js = arg3_converter(arg3); + } + const res_js = value(arg1_js, arg2_js, arg3_js); + if (res_converter) { + res_converter(res, res_js); + } + + } catch (ex) { + marshal_exception_to_cs(exc, ex); + } + }; + + marshal_function_to_cs_wrapper[bound_js_function_symbol] = true; + const bound_function_handle = mono_wasm_get_js_handle(marshal_function_to_cs_wrapper)!; + set_js_handle(arg, bound_function_handle); + set_arg_type(arg, MarshalerType.Function);//TODO or action ? +} + +export class TaskCallbackHolder implements IDisposable { + public promise: Promise<any> + + public constructor(promise: Promise<any>) { + this.promise = promise; + } + + dispose(): void { + teardown_managed_proxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (<any>this)[js_owned_gc_handle_symbol] === GCHandleNull; + } +} + +function _marshal_task_to_cs(arg: JSMarshalerArgument, value: Promise<any>, _?: JSMarshalerType, res_converter?: MarshalerToCs) { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + return; + } + mono_assert(isThenable(value), "Value is not a Promise"); + + const anyModule = Module as any; + const gc_handle: GCHandle = wrapped_cs_functions._create_task_callback(); + set_gc_handle(arg, gc_handle); + set_arg_type(arg, MarshalerType.Task); + const holder = new TaskCallbackHolder(value); + setup_managed_proxy(holder, gc_handle); + + value.then(data => { + const sp = anyModule.stackSave(); + try { + const args = anyModule.stackAlloc(JavaScriptMarshalerArgSize * 3); + const exc = get_arg(args, 0); + set_arg_type(exc, MarshalerType.None); + const res = get_arg(args, 1); + set_arg_type(res, MarshalerType.None); + set_gc_handle(res, <any>gc_handle); + const arg1 = get_arg(args, 2); + if (!res_converter) { + _marshal_cs_object_to_cs(arg1, data); + } else { + res_converter(arg1, data); + } + const fail = cwraps.mono_wasm_invoke_method_bound(runtimeHelpers.complete_task_method, args); + if (fail) throw new Error("ERR22: Unexpected error: " + conv_string(fail)); + if (is_args_exception(args)) throw marshal_exception_to_js(exc); + } finally { + anyModule.stackRestore(sp); + } + teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed, (holding promise instead would not work) + }).catch(reason => { + const sp = anyModule.stackSave(); + try { + const args = anyModule.stackAlloc(JavaScriptMarshalerArgSize * 3); + const res = get_arg(args, 1); + set_arg_type(res, MarshalerType.None); + set_gc_handle(res, gc_handle); + const exc = get_arg(args, 0); + if (typeof reason === "string" || reason === null || reason === undefined) { + reason = new Error(reason || ""); + } + marshal_exception_to_cs(exc, reason); + const fail = cwraps.mono_wasm_invoke_method_bound(runtimeHelpers.complete_task_method, args); + if (fail) throw new Error("ERR24: Unexpected error: " + conv_string(fail)); + if (is_args_exception(args)) throw marshal_exception_to_js(exc); + } finally { + anyModule.stackRestore(sp); + } + teardown_managed_proxy(holder, gc_handle); // this holds holder alive for finalizer, until the promise is freed + }); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function marshal_exception_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else if (value instanceof ManagedError) { + set_arg_type(arg, MarshalerType.Exception); + // this is managed exception round-trip + const gc_handle = assert_not_disposed(value); + set_gc_handle(arg, gc_handle); + } + else { + mono_assert(typeof value === "object" || typeof value === "string", () => `Value is not an Error ${typeof value}`); + set_arg_type(arg, MarshalerType.JSException); + const message = value.toString(); + _marshal_string_to_cs_impl(arg, message); + + const known_js_handle = value[cs_owned_js_handle_symbol]; + if (known_js_handle) { + set_js_handle(arg, known_js_handle); + } + else { + const js_handle = mono_wasm_get_js_handle(value)!; + set_js_handle(arg, js_handle); + } + } +} + +function _marshal_js_object_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === undefined || value === null) { + set_arg_type(arg, MarshalerType.None); + } + else { + // if value was ManagedObject, it would be double proxied, but the C# signature requires that + mono_assert(value[js_owned_gc_handle_symbol] === undefined, "JSObject proxy of ManagedObject proxy is not supported"); + mono_assert(typeof value === "function" || typeof value === "object", () => `JSObject proxy of ${typeof value} is not supported`); + + set_arg_type(arg, MarshalerType.JSObject); + const js_handle = mono_wasm_get_js_handle(value)!; + set_js_handle(arg, js_handle); + } +} + +function _marshal_cs_object_to_cs(arg: JSMarshalerArgument, value: any): void { + if (value === undefined || value === null) { + set_arg_type(arg, MarshalerType.None); + } + else { + const gc_handle = value[js_owned_gc_handle_symbol]; + const js_type = typeof (value); + if (gc_handle === undefined) { + if (js_type === "string" || js_type === "symbol") { + set_arg_type(arg, MarshalerType.String); + _marshal_string_to_cs_impl(arg, value); + } + else if (js_type === "number") { + set_arg_type(arg, MarshalerType.Double); + set_arg_f64(arg, value); + } + else if (js_type === "bigint") { + // we do it because not all bigint values could fit into Int64 + throw new Error("NotImplementedException: bigint"); + } + else if (js_type === "boolean") { + set_arg_type(arg, MarshalerType.Boolean); + set_arg_b8(arg, value); + } + else if (value instanceof Date) { + set_arg_type(arg, MarshalerType.DateTime); + set_arg_date(arg, value); + } + else if (value instanceof Error) { + set_arg_type(arg, MarshalerType.JSException); + const js_handle = mono_wasm_get_js_handle(value); + set_js_handle(arg, js_handle); + } + else if (value instanceof Uint8Array) { + _marshal_array_to_cs_impl(arg, value, MarshalerType.Byte); + } + else if (value instanceof Float64Array) { + _marshal_array_to_cs_impl(arg, value, MarshalerType.Double); + } + else if (value instanceof Int32Array) { + _marshal_array_to_cs_impl(arg, value, MarshalerType.Int32); + } + else if (Array.isArray(value)) { + _marshal_array_to_cs_impl(arg, value, MarshalerType.Object); + } + else if (value instanceof Int16Array + || value instanceof Int8Array + || value instanceof Uint8ClampedArray + || value instanceof Uint16Array + || value instanceof Uint32Array + || value instanceof Float32Array + ) { + throw new Error("NotImplementedException: TypedArray"); + } + else if (isThenable(value)) { + _marshal_task_to_cs(arg, value); + } + else if (value instanceof Span) { + throw new Error("NotImplementedException: Span"); + } + else if (js_type == "object") { + const js_handle = mono_wasm_get_js_handle(value); + set_arg_type(arg, MarshalerType.JSObject); + set_js_handle(arg, js_handle); + } + else { + throw new Error(`JSObject proxy is not supported for ${js_type} ${value}`); + } + } + else { + assert_not_disposed(value); + if (value instanceof ArraySegment) { + throw new Error("NotImplementedException: ArraySegment"); + } + else if (value instanceof ManagedError) { + set_arg_type(arg, MarshalerType.Exception); + set_gc_handle(arg, gc_handle); + } + else if (value instanceof ManagedObject) { + set_arg_type(arg, MarshalerType.Object); + set_gc_handle(arg, gc_handle); + } else { + throw new Error("NotImplementedException " + js_type); + } + } + } +} + +function _marshal_array_to_cs(arg: JSMarshalerArgument, value: Array<any> | TypedArray, sig?: JSMarshalerType): void { + mono_assert(!!sig, "Expected valid sig paramater"); + const element_type = get_signature_arg1_type(sig); + _marshal_array_to_cs_impl(arg, value, element_type); +} + +function _marshal_array_to_cs_impl(arg: JSMarshalerArgument, value: Array<any> | TypedArray, element_type: MarshalerType): void { + if (value === null || value === undefined) { + set_arg_type(arg, MarshalerType.None); + } + else { + const element_size = array_element_size(element_type); + mono_assert(element_size != -1, () => `Element type ${MarshalerType[element_type]} not supported`); + const length = value.length; + const buffer_length = element_size * length; + const buffer_ptr = <any>Module._malloc(buffer_length); + if (element_type == MarshalerType.String) { + mono_assert(Array.isArray(value), "Value is not an Array"); + _zero_region(buffer_ptr, buffer_length); + cwraps.mono_wasm_register_root(buffer_ptr, buffer_length, "marshal_array_to_cs"); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(<any>buffer_ptr, index); + _marshal_string_to_cs(element_arg, value[index]); + } + } + else if (element_type == MarshalerType.Object) { + mono_assert(Array.isArray(value), "Value is not an Array"); + _zero_region(buffer_ptr, buffer_length); + cwraps.mono_wasm_register_root(buffer_ptr, buffer_length, "marshal_array_to_cs"); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(<any>buffer_ptr, index); + _marshal_cs_object_to_cs(element_arg, value[index]); + } + } + else if (element_type == MarshalerType.JSObject) { + mono_assert(Array.isArray(value), "Value is not an Array"); + _zero_region(buffer_ptr, buffer_length); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(buffer_ptr, index); + _marshal_js_object_to_cs(element_arg, value[index]); + } + } + else if (element_type == MarshalerType.Byte) { + mono_assert(Array.isArray(value) || value instanceof Uint8Array, "Value is not an Array or Uint8Array"); + const targetView = Module.HEAPU8.subarray(<any>buffer_ptr, buffer_ptr + length); + targetView.set(value); + } + else if (element_type == MarshalerType.Int32) { + mono_assert(Array.isArray(value) || value instanceof Int32Array, "Value is not an Array or Int32Array"); + const targetView = Module.HEAP32.subarray(<any>buffer_ptr >> 2, (buffer_ptr >> 2) + length); + targetView.set(value); + } + else if (element_type == MarshalerType.Double) { + mono_assert(Array.isArray(value) || value instanceof Float64Array, "Value is not an Array or Float64Array"); + const targetView = Module.HEAPF64.subarray(<any>buffer_ptr >> 3, (buffer_ptr >> 3) + length); + targetView.set(value); + } + else { + throw new Error("not implemented"); + } + set_arg_intptr(arg, buffer_ptr); + set_arg_type(arg, MarshalerType.Array); + set_arg_element_type(arg, element_type); + set_arg_length(arg, value.length); + } +} + +function _marshal_span_to_cs(arg: JSMarshalerArgument, value: Span, sig?: JSMarshalerType): void { + mono_assert(!!sig, "Expected valid sig paramater"); + mono_assert(!value.isDisposed, "ObjectDisposedException"); + checkViewType(sig, value._viewType); + + set_arg_type(arg, MarshalerType.Span); + set_arg_intptr(arg, value._pointer); + set_arg_length(arg, value.length); +} + +// this only supports round-trip +function _marshal_array_segment_to_cs(arg: JSMarshalerArgument, value: ArraySegment, sig?: JSMarshalerType): void { + mono_assert(!!sig, "Expected valid sig paramater"); + const gc_handle = assert_not_disposed(value); + mono_assert(gc_handle, "Only roundtrip of ArraySegment instance created by C#"); + checkViewType(sig, value._viewType); + set_arg_type(arg, MarshalerType.ArraySegment); + set_arg_intptr(arg, value._pointer); + set_arg_length(arg, value.length); + set_gc_handle(arg, gc_handle); +} + +function checkViewType(sig: JSMarshalerType, viewType: MemoryViewType) { + const element_type = get_signature_arg1_type(sig); + if (element_type == MarshalerType.Byte) { + mono_assert(MemoryViewType.Byte == viewType, "Expected MemoryViewType.Byte"); + } + else if (element_type == MarshalerType.Int32) { + mono_assert(MemoryViewType.Int32 == viewType, "Expected MemoryViewType.Int32"); + } + else if (element_type == MarshalerType.Double) { + mono_assert(MemoryViewType.Double == viewType, "Expected MemoryViewType.Double"); + } + else { + throw new Error(`NotImplementedException ${MarshalerType[element_type]} `); + } +} + diff --git a/src/mono/wasm/runtime/marshal-to-js.ts b/src/mono/wasm/runtime/marshal-to-js.ts new file mode 100644 index 00000000000..47d240f1f13 --- /dev/null +++ b/src/mono/wasm/runtime/marshal-to-js.ts @@ -0,0 +1,581 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { PromiseControl, promise_control_symbol, create_cancelable_promise } from "./cancelable-promise"; +import cwraps from "./cwraps"; +import { _lookup_js_owned_object, mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle, setup_managed_proxy } from "./gc-handles"; +import { Module, runtimeHelpers } from "./imports"; +import { + ManagedObject, JSMarshalerArgument, ManagedError, JSMarshalerArguments, MarshalerToCs, MarshalerToJs, JSMarshalerType, + get_arg_gc_handle, get_arg_js_handle, get_arg_type, get_arg_i32, get_arg_f64, get_arg_i52, get_arg_i16, get_arg_u8, get_arg_f32, + get_arg_b8, get_arg_date, get_arg_length, set_js_handle, get_arg, set_arg_type, + get_signature_arg2_type, get_signature_arg1_type, get_signature_type, cs_to_js_marshalers, js_to_cs_marshalers, + get_signature_res_type, JavaScriptMarshalerArgSize, set_gc_handle, is_args_exception, get_arg_u16, array_element_size, get_string_root, ArraySegment, Span, MemoryViewType, get_signature_arg3_type, MarshalerType, get_arg_i64_big, get_arg_intptr, get_arg_element_type +} from "./marshal"; +import { conv_string, conv_string_root } from "./strings"; +import { mono_assert, JSHandleNull, GCHandleNull } from "./types"; +import { TypedArray } from "./types/emscripten"; + +export function initialize_marshalers_to_js(): void { + if (cs_to_js_marshalers.size == 0) { + cs_to_js_marshalers.set(MarshalerType.Array, _marshal_array_to_js); + cs_to_js_marshalers.set(MarshalerType.Span, _marshal_span_to_js); + cs_to_js_marshalers.set(MarshalerType.ArraySegment, _marshal_array_segment_to_js); + cs_to_js_marshalers.set(MarshalerType.Boolean, _marshal_bool_to_js); + cs_to_js_marshalers.set(MarshalerType.Byte, _marshal_byte_to_js); + cs_to_js_marshalers.set(MarshalerType.Char, _marshal_char_to_js); + cs_to_js_marshalers.set(MarshalerType.Int16, _marshal_int16_to_js); + cs_to_js_marshalers.set(MarshalerType.Int32, _marshal_int32_to_js); + cs_to_js_marshalers.set(MarshalerType.Int52, _marshal_int52_to_js); + cs_to_js_marshalers.set(MarshalerType.BigInt64, _marshal_bigint64_to_js); + cs_to_js_marshalers.set(MarshalerType.Single, _marshal_float_to_js); + cs_to_js_marshalers.set(MarshalerType.IntPtr, _marshal_intptr_to_js); + cs_to_js_marshalers.set(MarshalerType.Double, _marshal_double_to_js); + cs_to_js_marshalers.set(MarshalerType.String, _marshal_string_to_js); + cs_to_js_marshalers.set(MarshalerType.Exception, marshal_exception_to_js); + cs_to_js_marshalers.set(MarshalerType.JSException, marshal_exception_to_js); + cs_to_js_marshalers.set(MarshalerType.JSObject, _marshal_js_object_to_js); + cs_to_js_marshalers.set(MarshalerType.Object, _marshal_cs_object_to_js); + cs_to_js_marshalers.set(MarshalerType.DateTime, _marshal_datetime_to_js); + cs_to_js_marshalers.set(MarshalerType.DateTimeOffset, _marshal_datetime_to_js); + cs_to_js_marshalers.set(MarshalerType.Task, _marshal_task_to_js); + cs_to_js_marshalers.set(MarshalerType.Action, _marshal_delegate_to_js); + cs_to_js_marshalers.set(MarshalerType.Function, _marshal_delegate_to_js); + cs_to_js_marshalers.set(MarshalerType.None, _marshal_null_to_js); + cs_to_js_marshalers.set(MarshalerType.Void, _marshal_null_to_js); + cs_to_js_marshalers.set(MarshalerType.Discard, _marshal_null_to_js); + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function generate_arg_marshal_to_js(sig: JSMarshalerType, index: number, arg_offset: number, sig_offset: number, jsname: string, closure: any): { + converters: string, + call_body: string, + marshaler_type: MarshalerType +} { + let converters = ""; + let converter_types = ""; + let call_body = ""; + const converter_name = "converter" + index; + let converter_name_arg1 = "null"; + let converter_name_arg2 = "null"; + let converter_name_arg3 = "null"; + let converter_name_res = "null"; + + let marshaler_type = get_signature_type(sig); + if (marshaler_type === MarshalerType.None || marshaler_type === MarshalerType.Void) { + return { + converters, + call_body, + marshaler_type + }; + } + + const marshaler_type_res = get_signature_res_type(sig); + if (marshaler_type_res !== MarshalerType.None) { + const converter = cs_to_js_marshalers.get(marshaler_type_res); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_res} at ${index}`); + + if (marshaler_type != MarshalerType.Nullable) { + converter_name_res = "converter" + index + "_res"; + converters += ", " + converter_name_res; + converter_types += " " + MarshalerType[marshaler_type_res]; + closure[converter_name_res] = converter; + } else { + marshaler_type = marshaler_type_res; + } + } + + const marshaler_type_arg1 = get_signature_arg1_type(sig); + if (marshaler_type_arg1 !== MarshalerType.None) { + const converter = js_to_cs_marshalers.get(marshaler_type_arg1); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg1} at ${index}`); + + converter_name_arg1 = "converter" + index + "_arg1"; + converters += ", " + converter_name_arg1; + converter_types += " " + MarshalerType[marshaler_type_arg1]; + closure[converter_name_arg1] = converter; + } + + const marshaler_type_arg2 = get_signature_arg2_type(sig); + if (marshaler_type_arg2 !== MarshalerType.None) { + const converter = js_to_cs_marshalers.get(marshaler_type_arg2); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg2} at ${index}`); + + converter_name_arg2 = "converter" + index + "_arg2"; + converters += ", " + converter_name_arg2; + converter_types += " " + MarshalerType[marshaler_type_arg2]; + closure[converter_name_arg2] = converter; + } + + const marshaler_type_arg3 = get_signature_arg3_type(sig); + if (marshaler_type_arg3 !== MarshalerType.None) { + const converter = js_to_cs_marshalers.get(marshaler_type_arg3); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type_arg3} at ${index}`); + + converter_name_arg3 = "converter" + index + "_arg3"; + converters += ", " + converter_name_arg3; + converter_types += " " + MarshalerType[marshaler_type_arg3]; + closure[converter_name_arg3] = converter; + } + + const converter = cs_to_js_marshalers.get(marshaler_type); + mono_assert(converter && typeof converter === "function", () => `Unknow converter for type ${marshaler_type} at ${index} `); + + converters += ", " + converter_name; + converter_types += " " + MarshalerType[marshaler_type]; + closure[converter_name] = converter; + + if (marshaler_type == MarshalerType.Task) { + call_body = ` const ${jsname} = ${converter_name}(args + ${arg_offset}, signature + ${sig_offset}, ${converter_name_res}); // ${converter_types} \n`; + } else if (marshaler_type == MarshalerType.Action || marshaler_type == MarshalerType.Function) { + call_body = ` const ${jsname} = ${converter_name}(args + ${arg_offset}, signature + ${sig_offset}, ${converter_name_res}, ${converter_name_arg1}, ${converter_name_arg2}, ${converter_name_arg3}); // ${converter_types} \n`; + } else { + call_body = ` const ${jsname} = ${converter_name}(args + ${arg_offset}, signature + ${sig_offset}); // ${converter_types} \n`; + } + + return { + converters, + call_body, + marshaler_type + }; +} + +function _marshal_bool_to_js(arg: JSMarshalerArgument): boolean | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_b8(arg); +} + +function _marshal_byte_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_u8(arg); +} + +function _marshal_char_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_u16(arg); +} + +function _marshal_int16_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_i16(arg); +} + +function _marshal_int32_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_i32(arg); +} + +function _marshal_int52_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_i52(arg); +} + +function _marshal_bigint64_to_js(arg: JSMarshalerArgument): bigint | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_i64_big(arg); +} + +function _marshal_float_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_f32(arg); +} + +function _marshal_double_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_f64(arg); +} + +function _marshal_intptr_to_js(arg: JSMarshalerArgument): number | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + return get_arg_intptr(arg); +} + +function _marshal_null_to_js(): null { + return null; +} + +function _marshal_datetime_to_js(arg: JSMarshalerArgument): Date | null { + const type = get_arg_type(arg); + if (type === MarshalerType.None) { + return null; + } + return get_arg_date(arg); +} + +function _marshal_delegate_to_js(arg: JSMarshalerArgument, _?: JSMarshalerType, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs): Function | null { + const type = get_arg_type(arg); + if (type === MarshalerType.None) { + return null; + } + + const anyModule = Module as any; + const gc_handle = get_arg_gc_handle(arg); + let result = _lookup_js_owned_object(gc_handle); + if (result === null || result === undefined) { + // this will create new Function for the C# delegate + result = (arg1_js: any, arg2_js: any, arg3_js: any) => { + + const sp = anyModule.stackSave(); + try { + const args = anyModule.stackAlloc(JavaScriptMarshalerArgSize * 5); + const exc = get_arg(args, 0); + set_arg_type(exc, MarshalerType.None); + const res = get_arg(args, 1); + set_arg_type(res, MarshalerType.None); + set_gc_handle(res, <any>gc_handle); + const arg1 = get_arg(args, 2); + const arg2 = get_arg(args, 3); + const arg3 = get_arg(args, 4); + + if (arg1_converter) { + arg1_converter(arg1, arg1_js); + } + if (arg2_converter) { + arg2_converter(arg2, arg2_js); + } + if (arg3_converter) { + arg3_converter(arg3, arg3_js); + } + + const fail = cwraps.mono_wasm_invoke_method_bound(runtimeHelpers.call_delegate, args); + if (fail) throw new Error("ERR23: Unexpected error: " + conv_string(fail)); + if (is_args_exception(args)) throw marshal_exception_to_js(exc); + + if (res_converter) { + return res_converter(res); + } + + } finally { + anyModule.stackRestore(sp); + } + }; + + setup_managed_proxy(result, gc_handle); + } + + return result; +} + +function _marshal_task_to_js(arg: JSMarshalerArgument, _?: JSMarshalerType, res_converter?: MarshalerToJs): Promise<any> | null { + const type = get_arg_type(arg); + if (type === MarshalerType.None) { + return null; + } + + if (type !== MarshalerType.Task) { + + if (!res_converter) { + // when we arrived here from _marshal_cs_object_to_js + res_converter = cs_to_js_marshalers.get(type); + } + mono_assert(res_converter, () => `Unknow sub_converter for type ${MarshalerType[type]} `); + + // this is already resolved + const val = res_converter(arg); + return new Promise((resolve) => resolve(val)); + } + + const js_handle = get_arg_js_handle(arg); + // console.log("_marshal_task_to_js A" + js_handle); + if (js_handle == JSHandleNull) { + // this is already resolved void + return new Promise((resolve) => resolve(undefined)); + } + const promise = mono_wasm_get_jsobj_from_js_handle(js_handle); + mono_assert(!!promise, () => `ERR28: promise not found for js_handle: ${js_handle} `); + const promise_control = promise[promise_control_symbol] as PromiseControl; + mono_assert(!!promise_control, () => `ERR27: promise_control not found for js_handle: ${js_handle} `); + + const orig_resolve = promise_control.resolve; + promise_control.resolve = (argInner: JSMarshalerArgument) => { + // console.log("_marshal_task_to_js R" + js_handle); + const type = get_arg_type(argInner); + if (type === MarshalerType.None) { + orig_resolve(null); + return; + } + + if (!res_converter) { + // when we arrived here from _marshal_cs_object_to_js + res_converter = cs_to_js_marshalers.get(type); + } + mono_assert(res_converter, () => `Unknow sub_converter for type ${MarshalerType[type]}`); + + const js_value = res_converter!(argInner); + orig_resolve(js_value); + }; + + return promise; +} + +export function mono_wasm_marshal_promise(args: JSMarshalerArguments): void { + const exc = get_arg(args, 0); + const res = get_arg(args, 1); + const arg_handle = get_arg(args, 2); + const arg_value = get_arg(args, 3); + + const exc_type = get_arg_type(exc); + const value_type = get_arg_type(arg_value); + const js_handle = get_arg_js_handle(arg_handle); + + if (js_handle === JSHandleNull) { + const { promise, promise_control } = create_cancelable_promise(); + const js_handle = mono_wasm_get_js_handle(promise)!; + set_js_handle(res, js_handle); + + if (exc_type !== MarshalerType.None) { + // this is already failed task + const reason = marshal_exception_to_js(exc); + promise_control.reject(reason); + } + else if (value_type !== MarshalerType.Task) { + // this is already resolved task + const sub_converter = cs_to_js_marshalers.get(value_type); + mono_assert(sub_converter, () => `Unknow sub_converter for type ${MarshalerType[value_type]} `); + const data = sub_converter(arg_value); + promise_control.resolve(data); + } + } else { + // resolve existing promise + const promise = mono_wasm_get_jsobj_from_js_handle(js_handle); + mono_assert(!!promise, () => `ERR25: promise not found for js_handle: ${js_handle} `); + const promise_control = promise[promise_control_symbol] as PromiseControl; + mono_assert(!!promise_control, () => `ERR26: promise_control not found for js_handle: ${js_handle} `); + + if (exc_type !== MarshalerType.None) { + const reason = marshal_exception_to_js(exc); + promise_control.reject(reason); + } + else if (value_type !== MarshalerType.Task) { + // here we assume that resolve was wrapped with sub_converter inside _marshal_task_to_js + promise_control.resolve(arg_value); + } + } + set_arg_type(res, MarshalerType.Task); + set_arg_type(exc, MarshalerType.None); +} + +function _marshal_string_to_js(arg: JSMarshalerArgument): string | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + const root = get_string_root(arg); + try { + const value = conv_string_root(root); + return value; + } finally { + root.release(); + } +} + +export function marshal_exception_to_js(arg: JSMarshalerArgument): Error | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + if (type == MarshalerType.JSException) { + // this is JSException roundtrip + const js_handle = get_arg_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; + } + + const gc_handle = get_arg_gc_handle(arg); + let result = _lookup_js_owned_object(gc_handle); + if (result === null || result === undefined) { + // this will create new ManagedError + const message = _marshal_string_to_js(arg); + result = new ManagedError(message!); + + setup_managed_proxy(result, gc_handle); + } + + return result; +} + +function _marshal_js_object_to_js(arg: JSMarshalerArgument): any { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + const js_handle = get_arg_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; +} + +function _marshal_cs_object_to_js(arg: JSMarshalerArgument): any { + const marshaler_type = get_arg_type(arg); + if (marshaler_type == MarshalerType.None) { + return null; + } + if (marshaler_type == MarshalerType.JSObject) { + const js_handle = get_arg_js_handle(arg); + const js_obj = mono_wasm_get_jsobj_from_js_handle(js_handle); + return js_obj; + } + + if (marshaler_type == MarshalerType.Array) { + const element_type = get_arg_element_type(arg); + return _marshal_array_to_js_impl(arg, element_type); + } + + if (marshaler_type == MarshalerType.Object) { + const gc_handle = get_arg_gc_handle(arg); + if (gc_handle === GCHandleNull) { + return null; + } + + // see if we have js owned instance for this gc_handle already + let result = _lookup_js_owned_object(gc_handle); + + // If the JS object for this gc_handle was already collected (or was never created) + if (!result) { + result = new ManagedObject(); + setup_managed_proxy(result, gc_handle); + } + + return result; + } + + // other types + const converter = cs_to_js_marshalers.get(marshaler_type); + mono_assert(converter, () => `Unknow converter for type ${MarshalerType[marshaler_type]}`); + return converter(arg); +} + +function _marshal_array_to_js(arg: JSMarshalerArgument, sig?: JSMarshalerType): Array<any> | TypedArray | null { + mono_assert(!!sig, "Expected valid sig paramater"); + const element_type = get_signature_arg1_type(sig); + return _marshal_array_to_js_impl(arg, element_type); +} + +function _marshal_array_to_js_impl(arg: JSMarshalerArgument, element_type: MarshalerType): Array<any> | TypedArray | null { + const type = get_arg_type(arg); + if (type == MarshalerType.None) { + return null; + } + const elementSize = array_element_size(element_type); + mono_assert(elementSize != -1, () => `Element type ${MarshalerType[element_type]} not supported`); + const buffer_ptr = get_arg_intptr(arg); + const length = get_arg_length(arg); + let result: Array<any> | TypedArray | null = null; + if (element_type == MarshalerType.String) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(<any>buffer_ptr, index); + result[index] = _marshal_string_to_js(element_arg); + } + cwraps.mono_wasm_deregister_root(<any>buffer_ptr); + } + else if (element_type == MarshalerType.Object) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(<any>buffer_ptr, index); + result[index] = _marshal_cs_object_to_js(element_arg); + } + cwraps.mono_wasm_deregister_root(<any>buffer_ptr); + } + else if (element_type == MarshalerType.JSObject) { + result = new Array(length); + for (let index = 0; index < length; index++) { + const element_arg = get_arg(<any>buffer_ptr, index); + result[index] = _marshal_js_object_to_js(element_arg); + } + } + else if (element_type == MarshalerType.Byte) { + const sourceView = Module.HEAPU8.subarray(<any>buffer_ptr, buffer_ptr + length); + result = sourceView.slice();//copy + } + else if (element_type == MarshalerType.Int32) { + const sourceView = Module.HEAP32.subarray(buffer_ptr >> 2, (buffer_ptr >> 2) + length); + result = sourceView.slice();//copy + } + else if (element_type == MarshalerType.Double) { + const sourceView = Module.HEAPF64.subarray(buffer_ptr >> 3, (buffer_ptr >> 3) + length); + result = sourceView.slice();//copy + } + else { + throw new Error(`NotImplementedException ${MarshalerType[element_type]} `); + } + Module._free(<any>buffer_ptr); + return result; +} + +function _marshal_span_to_js(arg: JSMarshalerArgument, sig?: JSMarshalerType): Span { + mono_assert(!!sig, "Expected valid sig paramater"); + + const element_type = get_signature_arg1_type(sig); + const buffer_ptr = get_arg_intptr(arg); + const length = get_arg_length(arg); + let result: Span | null = null; + if (element_type == MarshalerType.Byte) { + result = new Span(<any>buffer_ptr, length, MemoryViewType.Byte); + } + else if (element_type == MarshalerType.Int32) { + result = new Span(<any>buffer_ptr, length, MemoryViewType.Int32); + } + else if (element_type == MarshalerType.Double) { + result = new Span(<any>buffer_ptr, length, MemoryViewType.Double); + } + else { + throw new Error(`NotImplementedException ${MarshalerType[element_type]} `); + } + return result; +} + +function _marshal_array_segment_to_js(arg: JSMarshalerArgument, sig?: JSMarshalerType): ArraySegment { + mono_assert(!!sig, "Expected valid sig paramater"); + + const element_type = get_signature_arg1_type(sig); + const buffer_ptr = get_arg_intptr(arg); + const length = get_arg_length(arg); + let result: ArraySegment | null = null; + if (element_type == MarshalerType.Byte) { + result = new ArraySegment(<any>buffer_ptr, length, MemoryViewType.Byte); + } + else if (element_type == MarshalerType.Int32) { + result = new ArraySegment(<any>buffer_ptr, length, MemoryViewType.Int32); + } + else if (element_type == MarshalerType.Double) { + result = new ArraySegment(<any>buffer_ptr, length, MemoryViewType.Double); + } + else { + throw new Error(`NotImplementedException ${MarshalerType[element_type]} `); + } + const gc_handle = get_arg_gc_handle(arg); + setup_managed_proxy(result, gc_handle); + + return result; +} diff --git a/src/mono/wasm/runtime/marshal.ts b/src/mono/wasm/runtime/marshal.ts new file mode 100644 index 00000000000..9a5e6a6dd63 --- /dev/null +++ b/src/mono/wasm/runtime/marshal.ts @@ -0,0 +1,509 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { js_owned_gc_handle_symbol, teardown_managed_proxy } from "./gc-handles"; +import { Module } from "./imports"; +import { getF32, getF64, getI16, getI32, getI64Big, getU16, getU32, getU8, setF32, setF64, setI16, setI32, setI64Big, setU16, setU32, setU8 } from "./memory"; +import { mono_wasm_new_external_root, WasmRoot } from "./roots"; +import { mono_assert, GCHandle, JSHandle, MonoObject, MonoString, GCHandleNull } from "./types"; +import { CharPtr, NativePointer, TypedArray, VoidPtr } from "./types/emscripten"; + +export const cs_to_js_marshalers = new Map<MarshalerType, MarshalerToJs>(); +export const js_to_cs_marshalers = new Map<MarshalerType, MarshalerToCs>(); +export const bound_cs_function_symbol = Symbol.for("wasm bound_cs_function"); +export const bound_js_function_symbol = Symbol.for("wasm bound_js_function"); + +/** + * JSFunctionSignature is pointer to [ + * Version: number, + * ArgumentCount: number, + * exc: { jsType: JsTypeFlags, type:MarshalerType, restype:MarshalerType, arg1type:MarshalerType, arg2type:MarshalerType, arg3type:MarshalerType} + * res: { jsType: JsTypeFlags, type:MarshalerType, restype:MarshalerType, arg1type:MarshalerType, arg2type:MarshalerType, arg3type:MarshalerType} + * arg1: { jsType: JsTypeFlags, type:MarshalerType, restype:MarshalerType, arg1type:MarshalerType, arg2type:MarshalerType, arg3type:MarshalerType} + * arg2: { jsType: JsTypeFlags, type:MarshalerType, restype:MarshalerType, arg1type:MarshalerType, arg2type:MarshalerType, arg3type:MarshalerType} + * ... + * ] + * + * Layout of the call stack frame buffers is array of JSMarshalerArgument + * JSMarshalerArguments is pointer to [ + * exc: {type:MarshalerType, handle: IntPtr, data: Int64|Ref*|Void* }, + * res: {type:MarshalerType, handle: IntPtr, data: Int64|Ref*|Void* }, + * arg1: {type:MarshalerType, handle: IntPtr, data: Int64|Ref*|Void* }, + * arg2: {type:MarshalerType, handle: IntPtr, data: Int64|Ref*|Void* }, + * ... + * ] + */ + + +export const JavaScriptMarshalerArgSize = 16; +export const JSMarshalerTypeSize = 32; +export const JSMarshalerSignatureHeaderSize = 4 + 4; // without Exception and Result + +export interface JSMarshalerArguments extends NativePointer { + __brand: "JSMarshalerArguments" +} + +export interface JSFunctionSignature extends NativePointer { + __brand: "JSFunctionSignatures" +} + +export interface JSMarshalerType extends NativePointer { + __brand: "JSMarshalerType" +} + +export interface JSMarshalerArgument extends NativePointer { + __brand: "JSMarshalerArgument" +} + +export function get_arg(args: JSMarshalerArguments, index: number): JSMarshalerArgument { + mono_assert(args, "Null args"); + return <any>args + (index * JavaScriptMarshalerArgSize); +} + +export function is_args_exception(args: JSMarshalerArguments): boolean { + mono_assert(args, "Null args"); + const exceptionType = get_arg_type(<any>args); + return exceptionType !== MarshalerType.None; +} + +export function get_sig(signature: JSFunctionSignature, index: number): JSMarshalerType { + mono_assert(signature, "Null signatures"); + return <any>signature + (index * JSMarshalerTypeSize) + JSMarshalerSignatureHeaderSize; +} + +export function get_signature_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null sig"); + return <any>getU32(sig); +} + +export function get_signature_res_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 16); +} + +export function get_signature_custom_code(sig: JSMarshalerType): CharPtr { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 8); +} + +export function get_signature_custom_code_len(sig: JSMarshalerType): number { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 12); +} + +export function get_signature_arg1_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 20); +} + +export function get_signature_arg2_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 24); +} + +export function get_signature_arg3_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null sig"); + return <any>getU32(<any>sig + 28); +} + +export function get_signature_argument_count(signature: JSFunctionSignature): number { + mono_assert(signature, "Null signatures"); + return <any>getI32(<any>signature + 4); +} + +export function get_signature_version(signature: JSFunctionSignature): number { + mono_assert(signature, "Null signatures"); + return <any>getI32(signature); +} + +export function get_sig_type(sig: JSMarshalerType): MarshalerType { + mono_assert(sig, "Null signatures"); + return <any>getU32(sig); +} + +export function get_arg_type(arg: JSMarshalerArgument): MarshalerType { + mono_assert(arg, "Null arg"); + const type = getU32(<any>arg + 12); + return <any>type; +} + +export function get_arg_element_type(arg: JSMarshalerArgument): MarshalerType { + mono_assert(arg, "Null arg"); + const type = getU32(<any>arg + 4); + return <any>type; +} + +export function set_arg_type(arg: JSMarshalerArgument, type: MarshalerType): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg + 12, type); +} + +export function set_arg_element_type(arg: JSMarshalerArgument, type: MarshalerType): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg + 4, type); +} + +export function get_arg_b8(arg: JSMarshalerArgument): boolean { + mono_assert(arg, "Null arg"); + return !!getU8(<any>arg); +} + +export function get_arg_u8(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getU8(<any>arg); +} + +export function get_arg_u16(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getU16(<any>arg); +} + +export function get_arg_i16(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getI16(<any>arg); +} + +export function get_arg_i32(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getI32(<any>arg); +} + +export function get_arg_intptr(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getU32(<any>arg); +} + +export function get_arg_i52(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + // we know that the range check and conversion from Int64 was be done on C# side + return getF64(<any>arg); +} + +export function get_arg_i64_big(arg: JSMarshalerArgument): bigint { + mono_assert(arg, "Null arg"); + return getI64Big(<any>arg); +} + +export function get_arg_date(arg: JSMarshalerArgument): Date { + mono_assert(arg, "Null arg"); + const unixTime = getF64(<any>arg); + const date = new Date(unixTime); + return date; +} + +export function get_arg_f32(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getF32(<any>arg); +} + +export function get_arg_f64(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return getF64(<any>arg); +} + +export function set_arg_b8(arg: JSMarshalerArgument, value: boolean): void { + mono_assert(arg, "Null arg"); + mono_assert(typeof value === "boolean", () => `Value is not a Boolean: ${value} (${typeof (value)})`); + setU8(<any>arg, value ? 1 : 0); +} + +export function set_arg_u8(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setU8(<any>arg, value); +} + +export function set_arg_u16(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setU16(<any>arg, value); +} + +export function set_arg_i16(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setI16(<any>arg, value); +} + +export function set_arg_i32(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setI32(<any>arg, value); +} + +export function set_arg_intptr(arg: JSMarshalerArgument, value: VoidPtr): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg, <any>value); +} + +export function set_arg_i52(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + mono_assert(Number.isSafeInteger(value), () => `Value is not an integer: ${value} (${typeof (value)})`); + // we know that conversion to Int64 would be done on C# side + setF64(<any>arg, value); +} + +export function set_arg_i64_big(arg: JSMarshalerArgument, value: bigint): void { + mono_assert(arg, "Null arg"); + setI64Big(<any>arg, value); +} + +export function set_arg_date(arg: JSMarshalerArgument, value: Date): void { + mono_assert(arg, "Null arg"); + // getTime() is always UTC + const unixTime = value.getTime(); + setF64(<any>arg, unixTime); +} + +export function set_arg_f64(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setF64(<any>arg, value); +} + +export function set_arg_f32(arg: JSMarshalerArgument, value: number): void { + mono_assert(arg, "Null arg"); + setF32(<any>arg, value); +} + +export function get_arg_js_handle(arg: JSMarshalerArgument): JSHandle { + mono_assert(arg, "Null arg"); + return <any>getU32(<any>arg + 4); +} + +export function set_js_handle(arg: JSMarshalerArgument, jsHandle: JSHandle): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg + 4, <any>jsHandle); +} + +export function get_arg_gc_handle(arg: JSMarshalerArgument): GCHandle { + mono_assert(arg, "Null arg"); + return <any>getU32(<any>arg + 4); +} + +export function set_gc_handle(arg: JSMarshalerArgument, gcHandle: GCHandle): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg + 4, <any>gcHandle); +} + +export function get_string_root(arg: JSMarshalerArgument): WasmRoot<MonoString> { + mono_assert(arg, "Null arg"); + return mono_wasm_new_external_root<MonoString>(<any>arg); +} + +export function get_arg_length(arg: JSMarshalerArgument): number { + mono_assert(arg, "Null arg"); + return <any>getI32(<any>arg + 8); +} + +export function set_arg_length(arg: JSMarshalerArgument, size: number): void { + mono_assert(arg, "Null arg"); + setI32(<any>arg + 8, size); +} + +export function set_root(arg: JSMarshalerArgument, root: WasmRoot<MonoObject>): void { + mono_assert(arg, "Null arg"); + setU32(<any>arg + 0, root.get_address()); +} + +export interface IDisposable { + dispose(): void; + get isDisposed(): boolean; +} + +export class ManagedObject implements IDisposable { + dispose(): void { + teardown_managed_proxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (<any>this)[js_owned_gc_handle_symbol] === GCHandleNull; + } + + toString(): string { + return `CsObject(gc_handle: ${(<any>this)[js_owned_gc_handle_symbol]})`; + } +} + +export class ManagedError extends Error implements IDisposable { + constructor(message: string) { + super(message); + } + + get stack(): string | undefined { + //todo implement lazy managed stack strace from this[js_owned_gc_handle_symbol]! + return super.stack; + } + + dispose(): void { + teardown_managed_proxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (<any>this)[js_owned_gc_handle_symbol] === GCHandleNull; + } + + toString(): string { + return `ManagedError(gc_handle: ${(<any>this)[js_owned_gc_handle_symbol]})`; + } +} + +export type MarshalerToJs = (arg: JSMarshalerArgument, sig?: JSMarshalerType, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs) => any; +export type MarshalerToCs = (arg: JSMarshalerArgument, value: any, sig?: JSMarshalerType, res_converter?: MarshalerToCs, arg1_converter?: MarshalerToJs, arg2_converter?: MarshalerToJs) => void; + +export function get_signature_marshaler(signature: JSFunctionSignature, index: number): JSHandle { + mono_assert(signature, "Null signatures"); + const sig = get_sig(signature, index); + return <any>getU32(<any>sig + 8); +} + + +export function array_element_size(element_type: MarshalerType): number { + return element_type == MarshalerType.Byte ? 1 + : element_type == MarshalerType.Int32 ? 4 + : element_type == MarshalerType.Int52 ? 8 + : element_type == MarshalerType.Double ? 8 + : element_type == MarshalerType.String ? JavaScriptMarshalerArgSize + : element_type == MarshalerType.Object ? JavaScriptMarshalerArgSize + : element_type == MarshalerType.JSObject ? JavaScriptMarshalerArgSize + : -1; +} + +export enum MemoryViewType { + Byte, + Int32, + Double, +} + +abstract class MemoryView implements IMemoryView, IDisposable { + protected constructor(public _pointer: VoidPtr, public _length: number, public _viewType: MemoryViewType) { + } + + abstract dispose(): void; + abstract get isDisposed(): boolean; + + _unsafe_create_view(): TypedArray { + // this view must be short lived so that it doesn't fail after wasm memory growth + // for that reason we also don't give the view out to end user and provide set/slice/copyTo API instead + const view = this._viewType == MemoryViewType.Byte ? new Uint8Array(Module.HEAPU8.buffer, <any>this._pointer, this._length) + : this._viewType == MemoryViewType.Int32 ? new Int32Array(Module.HEAP32.buffer, <any>this._pointer, this._length) + : this._viewType == MemoryViewType.Double ? new Float64Array(Module.HEAPF64.buffer, <any>this._pointer, this._length) + : null; + if (!view) throw new Error("NotImplementedException"); + return view; + } + + set(source: TypedArray, targetOffset?: number): void { + mono_assert(!this.isDisposed, "ObjectDisposedException"); + const targetView = this._unsafe_create_view(); + mono_assert(source && targetView && source.constructor === targetView.constructor, () => `Expected ${targetView.constructor}`); + targetView.set(source, targetOffset); + // TODO consider memory write barrier + } + + copyTo(target: TypedArray, sourceOffset?: number): void { + mono_assert(!this.isDisposed, "ObjectDisposedException"); + const sourceView = this._unsafe_create_view(); + mono_assert(target && sourceView && target.constructor === sourceView.constructor, () => `Expected ${sourceView.constructor}`); + const trimmedSource = sourceView.subarray(sourceOffset); + // TODO consider memory read barrier + target.set(trimmedSource); + } + + slice(start?: number, end?: number): TypedArray { + mono_assert(!this.isDisposed, "ObjectDisposedException"); + const sourceView = this._unsafe_create_view(); + // TODO consider memory read barrier + return sourceView.slice(start, end); + } + + get length(): number { + mono_assert(!this.isDisposed, "ObjectDisposedException"); + return this._length; + } + + get byteLength(): number { + mono_assert(!this.isDisposed, "ObjectDisposedException"); + return this._viewType == MemoryViewType.Byte ? this._length + : this._viewType == MemoryViewType.Int32 ? this._length << 2 + : this._viewType == MemoryViewType.Double ? this._length << 3 + : 0; + } +} + +export interface IMemoryView { + /** + * copies elements from provided source to the wasm memory. + * target has to have the elements of the same type as the underlying C# array. + * same as TypedArray.set() + */ + set(source: TypedArray, targetOffset?: number): void; + /** + * copies elements from wasm memory to provided target. + * target has to have the elements of the same type as the underlying C# array. + */ + copyTo(target: TypedArray, sourceOffset?: number): void; + /** + * same as TypedArray.slice() + */ + slice(start?: number, end?: number): TypedArray; + + get length(): number; + get byteLength(): number; +} + +export class Span extends MemoryView implements IDisposable { + private is_disposed = false; + public constructor(pointer: VoidPtr, length: number, viewType: MemoryViewType) { + super(pointer, length, viewType); + } + dispose(): void { + this.is_disposed = true; + } + get isDisposed(): boolean { + return this.is_disposed; + } +} + +export class ArraySegment extends MemoryView { + public constructor(pointer: VoidPtr, length: number, viewType: MemoryViewType) { + super(pointer, length, viewType); + } + + dispose(): void { + teardown_managed_proxy(this, GCHandleNull); + } + + get isDisposed(): boolean { + return (<any>this)[js_owned_gc_handle_symbol] === GCHandleNull; + } +} + +// please keep in sync with src\libraries\System.Runtime.InteropServices.JavaScript\src\System\Runtime\InteropServices\JavaScript\MarshalerType.cs +export enum MarshalerType { + None = 0, + Void = 1, + Discard, + Boolean, + Byte, + Char, + Int16, + Int32, + Int52, + BigInt64, + Double, + Single, + IntPtr, + JSObject, + Object, + String, + Exception, + DateTime, + DateTimeOffset, + + Nullable, + Task, + Array, + ArraySegment, + Span, + Action, + Function, + + // only on runtime + JSException, +}
\ No newline at end of file diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 776a8eef168..3b3ec8f1486 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -16,6 +16,8 @@ import { find_corlib_class } from "./class-loader"; import { VoidPtr, CharPtr } from "./types/emscripten"; import { DotnetPublicAPI } from "./exports"; import { mono_on_abort } from "./run"; +import { initialize_marshalers_to_cs } from "./marshal-to-cs"; +import { initialize_marshalers_to_js } from "./marshal-to-js"; import { mono_wasm_new_root } from "./roots"; import { init_crypto } from "./crypto-worker"; import { init_polyfills } from "./polyfills"; @@ -452,6 +454,21 @@ export function bindings_lazy_init(): void { if (!runtimeHelpers.get_call_sig_ref) throw "Can't find GetCallSignatureRef method"; + runtimeHelpers.complete_task_method = get_method("CompleteTask"); + if (!runtimeHelpers.complete_task_method) + throw "Can't find CompleteTask method"; + + runtimeHelpers.create_task_method = get_method("CreateTaskCallback"); + if (!runtimeHelpers.create_task_method) + throw "Can't find CreateTaskCallback method"; + + runtimeHelpers.call_delegate = get_method("CallDelegate"); + if (!runtimeHelpers.call_delegate) + throw "Can't find CallDelegate method"; + + initialize_marshalers_to_js(); + initialize_marshalers_to_cs(); + _create_primitive_converters(); runtimeHelpers._box_root = mono_wasm_new_root<MonoObject>(); diff --git a/src/mono/wasm/runtime/types.ts b/src/mono/wasm/runtime/types.ts index 649de856d70..8e3970722aa 100644 --- a/src/mono/wasm/runtime/types.ts +++ b/src/mono/wasm/runtime/types.ts @@ -132,6 +132,9 @@ export const enum AssetBehaviours { export type RuntimeHelpers = { get_call_sig_ref: MonoMethod; + complete_task_method: MonoMethod; + create_task_method: MonoMethod; + call_delegate: MonoMethod; runtime_interop_namespace: string; runtime_interop_exports_classname: string; runtime_interop_exports_class: MonoClass; diff --git a/src/mono/wasm/runtime/web-socket.ts b/src/mono/wasm/runtime/web-socket.ts index caaa9bd3930..feeebf0f449 100644 --- a/src/mono/wasm/runtime/web-socket.ts +++ b/src/mono/wasm/runtime/web-socket.ts @@ -1,19 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { mono_wasm_new_root, mono_wasm_new_external_root } from "./roots"; -import { setI32 } from "./memory"; import { prevent_timer_throttling } from "./scheduling"; import { Queue } from "./queue"; -import { PromiseControl, _create_cancelable_promise } from "./cancelable-promise"; -import { mono_array_root_to_js_array, _wrap_delegate_root_as_function } from "./cs-to-js"; -import { mono_wasm_get_jsobj_from_js_handle, mono_wasm_get_js_handle } from "./gc-handles"; -import { _wrap_js_thenable_as_task_root } from "./js-to-cs"; -import { wrap_error_root } from "./method-calls"; -import { conv_string_root } from "./strings"; -import { JSHandle, MonoArray, MonoObject, MonoString, MonoObjectRef } from "./types"; -import { Module } from "./imports"; -import { Int32Ptr, VoidPtr } from "./types/emscripten"; +import { PromiseControl, create_cancelable_promise } from "./cancelable-promise"; +import { mono_assert } from "./types"; +import { ArraySegment, IDisposable } from "./marshal"; const wasm_ws_pending_send_buffer = Symbol.for("wasm ws_pending_send_buffer"); const wasm_ws_pending_send_buffer_offset = Symbol.for("wasm ws_pending_send_buffer_offset"); @@ -30,302 +22,210 @@ let _text_encoder_utf8: TextEncoder | undefined = undefined; const ws_send_buffer_blocking_threshold = 65536; const emptyBuffer = new Uint8Array(); -export function mono_wasm_web_socket_open_ref(uri_address: MonoObjectRef, subProtocols: MonoObjectRef, on_close: MonoObjectRef, web_socket_js_handle: Int32Ptr, thenable_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); - const uri_root = mono_wasm_new_external_root<MonoString>(uri_address); - const sub_root = mono_wasm_new_external_root<MonoArray>(subProtocols); - const on_close_root = mono_wasm_new_root(); - on_close_root.copy_from_address(on_close); - try { - const js_uri = conv_string_root(uri_root); - if (!js_uri) { - wrap_error_root(is_exception, "ERR12: Invalid uri '" + uri_root.value + "'", result_root); - return; - } +export function ws_wasm_create(uri: string, sub_protocols: string[] | null, onClosed: (code: number, reason: string) => void): WebSocketExtension { + mono_assert(uri && typeof uri === "string", () => `ERR12: Invalid uri ${typeof uri}`); + + const ws = new globalThis.WebSocket(uri, sub_protocols || undefined) as WebSocketExtension; + const { promise_control: open_promise_control } = create_cancelable_promise(); + + ws[wasm_ws_pending_receive_event_queue] = new Queue(); + ws[wasm_ws_pending_receive_promise_queue] = new Queue(); + ws[wasm_ws_pending_open_promise] = open_promise_control; + ws[wasm_ws_pending_send_promises] = []; + ws[wasm_ws_pending_close_promises] = []; + ws.binaryType = "arraybuffer"; + const local_on_open = () => { + if (ws[wasm_ws_is_aborted]) return; + open_promise_control.resolve(ws); + prevent_timer_throttling(); + }; + const local_on_message = (ev: MessageEvent) => { + if (ws[wasm_ws_is_aborted]) return; + _mono_wasm_web_socket_on_message(ws, ev); + prevent_timer_throttling(); + }; + const local_on_close = (ev: CloseEvent) => { + ws.removeEventListener("message", local_on_message); + if (ws[wasm_ws_is_aborted]) return; + if (onClosed) onClosed(ev.code, ev.reason); + + // this reject would not do anything if there was already "open" before it. + open_promise_control.reject(ev.reason); - const js_subs = mono_array_root_to_js_array(sub_root); - - const js_on_close = _wrap_delegate_root_as_function(on_close_root)!; - - const ws = new globalThis.WebSocket(js_uri, <any>js_subs) as WebSocketExtension; - const { promise, promise_control: open_promise_control } = _create_cancelable_promise(); - - ws[wasm_ws_pending_receive_event_queue] = new Queue(); - ws[wasm_ws_pending_receive_promise_queue] = new Queue(); - ws[wasm_ws_pending_open_promise] = open_promise_control; - ws[wasm_ws_pending_send_promises] = []; - ws[wasm_ws_pending_close_promises] = []; - ws.binaryType = "arraybuffer"; - const local_on_open = () => { - if (ws[wasm_ws_is_aborted]) return; - open_promise_control.resolve(null); - prevent_timer_throttling(); - }; - const local_on_message = (ev: MessageEvent) => { - if (ws[wasm_ws_is_aborted]) return; - _mono_wasm_web_socket_on_message(ws, ev); - prevent_timer_throttling(); - }; - const local_on_close = (ev: CloseEvent) => { - ws.removeEventListener("message", local_on_message); - if (ws[wasm_ws_is_aborted]) return; - js_on_close(ev.code, ev.reason); - - // this reject would not do anything if there was already "open" before it. - open_promise_control.reject(ev.reason); - - for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { - close_promise_control.resolve(); - } + for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { + close_promise_control.resolve(); + } - // send close to any pending receivers, to wake them - const receive_promise_queue = ws[wasm_ws_pending_receive_promise_queue]; - receive_promise_queue.drain((receive_promise_control) => { - const response_ptr = receive_promise_control.response_ptr; - setI32(<any>response_ptr + 0, 0);// count - setI32(<any>response_ptr + 4, 2);// type:close - setI32(<any>response_ptr + 8, 1);// end_of_message: true - receive_promise_control.resolve(null); - }); - }; - const local_on_error = (ev: any) => { - open_promise_control.reject(ev.message); - }; - ws.addEventListener("message", local_on_message); - ws.addEventListener("open", local_on_open, { once: true }); - ws.addEventListener("close", local_on_close, { once: true }); - ws.addEventListener("error", local_on_error, { once: true }); - const ws_js_handle = mono_wasm_get_js_handle(ws); - Module.setValue(web_socket_js_handle, <any>ws_js_handle, "i32"); - - const { then_js_handle } = _wrap_js_thenable_as_task_root(promise, result_root); - // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC - Module.setValue(thenable_js_handle, <any>then_js_handle, "i32"); - } - catch (ex) { - wrap_error_root(is_exception, ex, result_root); - } - finally { - result_root.release(); - uri_root.release(); - sub_root.release(); - on_close_root.release(); - } + // send close to any pending receivers, to wake them + const receive_promise_queue = ws[wasm_ws_pending_receive_promise_queue]; + receive_promise_queue.drain((receive_promise_control) => { + + const response = new Int32Array([ + 0,// count + 2, // type:close + 1]);// end_of_message: true + receive_promise_control.responseView.set(response); + receive_promise_control.resolve(null); + }); + }; + const local_on_error = (ev: any) => { + open_promise_control.reject(ev.message || "WebSocket error"); + }; + ws.addEventListener("message", local_on_message); + ws.addEventListener("open", local_on_open, { once: true }); + ws.addEventListener("close", local_on_close, { once: true }); + ws.addEventListener("error", local_on_error, { once: true }); + + return ws; } -export function mono_wasm_web_socket_send(webSocket_js_handle: JSHandle, buffer_ptr: VoidPtr, offset: number, length: number, message_type: number, end_of_message: boolean, thenable_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); - try { - const ws = mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle); - if (!ws) - throw new Error("ERR17: Invalid JS object handle " + webSocket_js_handle); +export function ws_wasm_open(ws: WebSocketExtension): Promise<void> | null { + mono_assert(!!ws, "ERR17: expected ws instance"); + const open_promise_control = ws[wasm_ws_pending_open_promise]; + return open_promise_control.promise; +} - if (ws.readyState != WebSocket.OPEN) { - throw new Error("InvalidState: The WebSocket is not connected."); - } +export function ws_wasm_send(ws: WebSocketExtension, bufferView: ArraySegment, message_type: number, end_of_message: boolean): Promise<void> | null { + mono_assert(!!ws, "ERR17: expected ws instance"); - const whole_buffer = _mono_wasm_web_socket_send_buffering(ws, buffer_ptr, offset, length, message_type, end_of_message); + const whole_buffer = _mono_wasm_web_socket_send_buffering(ws, bufferView, message_type, end_of_message); - if (!end_of_message || !whole_buffer) { - result_root.clear(); // we are done buffering synchronously, no promise - return; - } - _mono_wasm_web_socket_send_and_wait(ws, whole_buffer, thenable_js_handle, result_address); - } - catch (ex) { - wrap_error_root(is_exception, ex, result_root); - } - finally { - result_root.release(); + if (!end_of_message || !whole_buffer) { + return null; } + + return _mono_wasm_web_socket_send_and_wait(ws, whole_buffer, bufferView); } -export function mono_wasm_web_socket_receive(webSocket_js_handle: JSHandle, buffer_ptr: VoidPtr, offset: number, length: number, response_ptr: VoidPtr, thenable_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); +export function ws_wasm_receive(ws: WebSocketExtension, bufferView: ArraySegment, responseView: ArraySegment): Promise<void> | null { + mono_assert(!!ws, "ERR18: expected ws instance"); - try { - const ws = mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle); - if (!ws) - throw new Error("ERR18: Invalid JS object handle " + webSocket_js_handle); - const receive_event_queue = ws[wasm_ws_pending_receive_event_queue]; - const receive_promise_queue = ws[wasm_ws_pending_receive_promise_queue]; + const receive_event_queue = ws[wasm_ws_pending_receive_event_queue]; + const receive_promise_queue = ws[wasm_ws_pending_receive_promise_queue]; - const readyState = ws.readyState; - if (readyState != WebSocket.OPEN && readyState != WebSocket.CLOSING) { - throw new Error("InvalidState: The WebSocket is not connected."); - } + const readyState = ws.readyState; + if (readyState != WebSocket.OPEN && readyState != WebSocket.CLOSING) { + throw new Error("InvalidState: The WebSocket is not connected."); + } - if (receive_event_queue.getLength()) { - if (receive_promise_queue.getLength() != 0) { - throw new Error("ERR20: Invalid WS state");// assert - } - // finish synchronously - _mono_wasm_web_socket_receive_buffering(receive_event_queue, buffer_ptr, offset, length, response_ptr); + if (receive_event_queue.getLength()) { + mono_assert(receive_promise_queue.getLength() == 0, "ERR20: Invalid WS state"); - Module.setValue(thenable_js_handle, 0, "i32"); - result_root.clear(); - return; - } - const { promise, promise_control } = _create_cancelable_promise(undefined, undefined); - const receive_promise_control = promise_control as ReceivePromiseControl; - receive_promise_control.buffer_ptr = buffer_ptr; - receive_promise_control.buffer_offset = offset; - receive_promise_control.buffer_length = length; - receive_promise_control.response_ptr = response_ptr; - receive_promise_queue.enqueue(receive_promise_control); - - const { then_js_handle } = _wrap_js_thenable_as_task_root(promise, result_root); - // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC - Module.setValue(thenable_js_handle, <any>then_js_handle, "i32"); - } - catch (ex) { - wrap_error_root(is_exception, ex, result_root); - } - finally { - result_root.release(); + // finish synchronously + _mono_wasm_web_socket_receive_buffering(receive_event_queue, bufferView, responseView); + + return null; } -} + const { promise, promise_control } = create_cancelable_promise(undefined, undefined); + const receive_promise_control = promise_control as ReceivePromiseControl; + receive_promise_control.bufferView = bufferView; + receive_promise_control.responseView = responseView; + receive_promise_queue.enqueue(receive_promise_control); -export function mono_wasm_web_socket_close_ref(webSocket_js_handle: JSHandle, code: number, reason: MonoObjectRef, wait_for_close_received: boolean, thenable_js_handle: Int32Ptr, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); - const reason_root = mono_wasm_new_external_root<MonoString>(reason); - try { - const ws = mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle); - if (!ws) - throw new Error("ERR19: Invalid JS object handle " + webSocket_js_handle); + return promise; +} - if (ws.readyState == WebSocket.CLOSED) { - result_root.clear(); - return; - } +export function ws_wasm_close(ws: WebSocketExtension, code: number, reason: string | null, wait_for_close_received: boolean): Promise<void> | null { + mono_assert(!!ws, "ERR19: expected ws instance"); - const js_reason = conv_string_root(reason_root); - if (wait_for_close_received) { - const { promise, promise_control } = _create_cancelable_promise(); - ws[wasm_ws_pending_close_promises].push(promise_control); + if (ws.readyState == WebSocket.CLOSED) { + return null; + } - if (typeof (js_reason) === "string") { - ws.close(code, js_reason); - } else { - ws.close(code); - } + if (wait_for_close_received) { + const { promise, promise_control } = create_cancelable_promise(); + ws[wasm_ws_pending_close_promises].push(promise_control); - const { then_js_handle } = _wrap_js_thenable_as_task_root(promise, result_root); - // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC - Module.setValue(thenable_js_handle, <any>then_js_handle, "i32"); - } - else { - if (!mono_wasm_web_socket_close_warning) { - mono_wasm_web_socket_close_warning = true; - console.warn("WARNING: Web browsers do not support closing the output side of a WebSocket. CloseOutputAsync has closed the socket and discarded any incoming messages."); - } - if (typeof (js_reason) === "string") { - ws.close(code, js_reason); - } else { - ws.close(code); - } - Module.setValue(thenable_js_handle, 0, "i32"); - result_root.clear(); + if (typeof reason === "string") { + ws.close(code, reason); + } else { + ws.close(code); } + return promise; } - catch (ex) { - wrap_error_root(is_exception, ex, result_root); - } - finally { - result_root.release(); - reason_root.release(); - } -} - -export function mono_wasm_web_socket_abort(webSocket_js_handle: JSHandle, is_exception: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); - try { - const ws = mono_wasm_get_jsobj_from_js_handle(webSocket_js_handle) as WebSocketExtension; - if (!ws) - throw new Error("ERR18: Invalid JS object handle " + webSocket_js_handle); - - ws[wasm_ws_is_aborted] = true; - const open_promise_control = ws[wasm_ws_pending_open_promise]; - if (open_promise_control) { - open_promise_control.reject("OperationCanceledException"); - } - for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { - close_promise_control.reject("OperationCanceledException"); + else { + if (!mono_wasm_web_socket_close_warning) { + mono_wasm_web_socket_close_warning = true; + console.warn("WARNING: Web browsers do not support closing the output side of a WebSocket. CloseOutputAsync has closed the socket and discarded any incoming messages."); } - for (const send_promise_control of ws[wasm_ws_pending_send_promises]) { - send_promise_control.reject("OperationCanceledException"); + if (typeof reason === "string") { + ws.close(code, reason); + } else { + ws.close(code); } + return null; + } +} - ws[wasm_ws_pending_receive_promise_queue].drain(receive_promise_control => { - receive_promise_control.reject("OperationCanceledException"); - }); - - // this is different from Managed implementation - ws.close(1000, "Connection was aborted."); +export function ws_wasm_abort(ws: WebSocketExtension): void { + mono_assert(!!ws, "ERR18: expected ws instance"); - result_root.clear(); + ws[wasm_ws_is_aborted] = true; + const open_promise_control = ws[wasm_ws_pending_open_promise]; + if (open_promise_control) { + open_promise_control.reject("OperationCanceledException"); } - catch (ex) { - wrap_error_root(is_exception, ex, result_root); + for (const close_promise_control of ws[wasm_ws_pending_close_promises]) { + close_promise_control.reject("OperationCanceledException"); } - finally { - result_root.release(); + for (const send_promise_control of ws[wasm_ws_pending_send_promises]) { + send_promise_control.reject("OperationCanceledException"); } + + ws[wasm_ws_pending_receive_promise_queue].drain(receive_promise_control => { + receive_promise_control.reject("OperationCanceledException"); + }); + + // this is different from Managed implementation + ws.close(1000, "Connection was aborted."); } -function _mono_wasm_web_socket_send_and_wait(ws: WebSocketExtension, buffer: Uint8Array | string, thenable_js_handle: Int32Ptr, result_address: MonoObjectRef): void { - const result_root = mono_wasm_new_external_root<MonoObject>(result_address); - try { - // send and return promise - ws.send(buffer); - ws[wasm_ws_pending_send_buffer] = null; - - // if the remaining send buffer is small, we don't block so that the throughput doesn't suffer. - // Otherwise we block so that we apply some backpresure to the application sending large data. - // this is different from Managed implementation - if (ws.bufferedAmount < ws_send_buffer_blocking_threshold) { - result_root.clear(); - return; - } +function _mono_wasm_web_socket_send_and_wait(ws: WebSocketExtension, buffer: Uint8Array | string, managedBuffer: IDisposable): Promise<void> | null { + // send and return promise + ws.send(buffer); + managedBuffer.dispose(); + ws[wasm_ws_pending_send_buffer] = null; + + // if the remaining send buffer is small, we don't block so that the throughput doesn't suffer. + // Otherwise we block so that we apply some backpresure to the application sending large data. + // this is different from Managed implementation + if (ws.bufferedAmount < ws_send_buffer_blocking_threshold) { + return null; + } - // block the promise/task until the browser passed the buffer to OS - const { promise, promise_control } = _create_cancelable_promise(); - const pending = ws[wasm_ws_pending_send_promises]; - pending.push(promise_control); + // block the promise/task until the browser passed the buffer to OS + const { promise, promise_control } = create_cancelable_promise(); + const pending = ws[wasm_ws_pending_send_promises]; + pending.push(promise_control); - let nextDelay = 1; - const polling_check = () => { - // was it all sent yet ? - if (ws.bufferedAmount === 0) { - promise_control.resolve(null); - } - else if (ws.readyState != WebSocket.OPEN) { - // only reject if the data were not sent - // bufferedAmount does not reset to zero once the connection closes - promise_control.reject("InvalidState: The WebSocket is not connected."); - } - else if (!promise_control.isDone) { - globalThis.setTimeout(polling_check, nextDelay); - // exponentially longer delays, up to 1000ms - nextDelay = Math.min(nextDelay * 1.5, 1000); - return; - } - // remove from pending - const index = pending.indexOf(promise_control); - if (index > -1) { - pending.splice(index, 1); - } - }; + let nextDelay = 1; + const polling_check = () => { + // was it all sent yet ? + if (ws.bufferedAmount === 0) { + promise_control.resolve(null); + } + else if (ws.readyState != WebSocket.OPEN) { + // only reject if the data were not sent + // bufferedAmount does not reset to zero once the connection closes + promise_control.reject("InvalidState: The WebSocket is not connected."); + } + else if (!promise_control.isDone) { + globalThis.setTimeout(polling_check, nextDelay); + // exponentially longer delays, up to 1000ms + nextDelay = Math.min(nextDelay * 1.5, 1000); + return; + } + // remove from pending + const index = pending.indexOf(promise_control); + if (index > -1) { + pending.splice(index, 1); + } + }; - globalThis.setTimeout(polling_check, 0); + globalThis.setTimeout(polling_check, 0); - const { then_js_handle } = _wrap_js_thenable_as_task_root(promise, result_root); - // task_ptr above is not rooted, we need to return it to mono without any intermediate mono call which could cause GC - Module.setValue(thenable_js_handle, <any>then_js_handle, "i32"); - } finally { - result_root.release(); - } + return promise; } function _mono_wasm_web_socket_on_message(ws: WebSocketExtension, event: MessageEvent) { @@ -361,36 +261,37 @@ function _mono_wasm_web_socket_on_message(ws: WebSocketExtension, event: Message while (promise_queue.getLength() && event_queue.getLength()) { const promise_control = promise_queue.dequeue()!; _mono_wasm_web_socket_receive_buffering(event_queue, - promise_control.buffer_ptr, promise_control.buffer_offset, promise_control.buffer_length, - promise_control.response_ptr); + promise_control.bufferView, promise_control.responseView); promise_control.resolve(null); } prevent_timer_throttling(); } -function _mono_wasm_web_socket_receive_buffering(event_queue: Queue<any>, buffer_ptr: VoidPtr, buffer_offset: number, buffer_length: number, response_ptr: VoidPtr) { +function _mono_wasm_web_socket_receive_buffering(event_queue: Queue<any>, bufferView: ArraySegment, responseView: ArraySegment) { const event = event_queue.peek(); - const count = Math.min(buffer_length, event.data.length - event.offset); + const count = Math.min(bufferView.length, event.data.length - event.offset); if (count > 0) { - const targetView = Module.HEAPU8.subarray(<any>buffer_ptr + buffer_offset, <any>buffer_ptr + buffer_offset + buffer_length); const sourceView = event.data.subarray(event.offset, event.offset + count); - targetView.set(sourceView, 0); + bufferView.set(sourceView, 0); event.offset += count; } const end_of_message = event.data.length === event.offset ? 1 : 0; if (end_of_message) { event_queue.dequeue(); } - setI32(<any>response_ptr + 0, count); - setI32(<any>response_ptr + 4, event.type); - setI32(<any>response_ptr + 8, end_of_message); + + const response = new Int32Array([count, event.type, end_of_message]); + responseView.set(response); + + bufferView.dispose(); + responseView.dispose(); } -function _mono_wasm_web_socket_send_buffering(ws: WebSocketExtension, buffer_ptr: VoidPtr, buffer_offset: number, length: number, message_type: number, end_of_message: boolean): Uint8Array | string | null { +function _mono_wasm_web_socket_send_buffering(ws: WebSocketExtension, bufferView: ArraySegment, message_type: number, end_of_message: boolean): Uint8Array | string | null { let buffer = ws[wasm_ws_pending_send_buffer]; let offset = 0; - const message_ptr = <any>buffer_ptr + buffer_offset; + const length = bufferView.length; if (buffer) { offset = ws[wasm_ws_pending_send_buffer_offset]; @@ -398,15 +299,14 @@ function _mono_wasm_web_socket_send_buffering(ws: WebSocketExtension, buffer_ptr message_type = ws[wasm_ws_pending_send_buffer_type]; // if not empty message, append to existing buffer if (length !== 0) { - const view = Module.HEAPU8.subarray(message_ptr, message_ptr + length); if (offset + length > buffer.length) { const newbuffer = new Uint8Array((offset + length + 50) * 1.5); // exponential growth newbuffer.set(buffer, 0);// copy previous buffer - newbuffer.set(view, offset);// append copy at the end + bufferView.copyTo(newbuffer.subarray(offset));// append copy at the end ws[wasm_ws_pending_send_buffer] = buffer = newbuffer; } else { - buffer.set(view, offset);// append copy at the end + bufferView.copyTo(buffer.subarray(offset));// append copy at the end } offset += length; ws[wasm_ws_pending_send_buffer_offset] = offset; @@ -415,8 +315,7 @@ function _mono_wasm_web_socket_send_buffering(ws: WebSocketExtension, buffer_ptr else if (!end_of_message) { // create new buffer if (length !== 0) { - const view = Module.HEAPU8.subarray(message_ptr, message_ptr + length); - buffer = new Uint8Array(view); // copy + buffer = <Uint8Array>bufferView.slice(); // copy offset = length; ws[wasm_ws_pending_send_buffer_offset] = offset; ws[wasm_ws_pending_send_buffer] = buffer; @@ -424,10 +323,9 @@ function _mono_wasm_web_socket_send_buffering(ws: WebSocketExtension, buffer_ptr ws[wasm_ws_pending_send_buffer_type] = message_type; } else { - // use the buffer only localy if (length !== 0) { - const memoryView = Module.HEAPU8.subarray(message_ptr, message_ptr + length); - buffer = memoryView; // send will make a copy + // we could use the unsafe view, because it will be immediately used in ws.send() + buffer = <Uint8Array>bufferView._unsafe_create_view(); offset = length; } } @@ -469,10 +367,8 @@ type WebSocketExtension = WebSocket & { } type ReceivePromiseControl = PromiseControl & { - response_ptr: VoidPtr - buffer_ptr: VoidPtr - buffer_offset: number - buffer_length: number + bufferView: ArraySegment, + responseView: ArraySegment } type Message = { |