-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SUPER DRAFT] New API for invoking JS functions from C# #60765
Conversation
I couldn't figure out the best area label to add to this PR. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to 'arch-wasm': @lewing Issue DetailsThis PR (which relies on the custom marshalers PR, #47640) introduces a new icall ( The icall enforces some rules to produce good performance:
The new icall offers some new features as part of the deal:
For scenarios not covered by the new icall, the intended solution is to expose higher-level convenience wrappers that provide the bells and whistles people expect. This keeps the icall slim and fast so that in use cases where you want maximum performance, you can get it. At present the PR provides As a proof of concept, the PR changes
|
Some rough performance measurements:
|
cc @jeromelaban @SteveSandersonMS will this new invoke API satisfy your needs? My hope is that you will be able to migrate to use it under the hood and get improved performance. |
@@ -52,6 +53,26 @@ export function mono_wasm_new_root_buffer_from_pointer(offset: VoidPtr, capacity | |||
return new WasmRootBuffer(offset, capacity, false, name); | |||
} | |||
|
|||
/** | |||
* Allocates a WasmRoot pointing to a root provided and controlled by external code. | |||
* Releasing this root will not do anything, but you still need to call .release(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please explain why we need this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
External callers have object references sitting on the stack, but the GC can still move them. So we want to wrap that external stack root in a WasmRoot-api type for JS to consume. If we use a JS-owned WasmRoot instance we have to copy the ref, which is inefficient (and might have a race?)
} | ||
} | ||
|
||
export function _walk_global_scope_to_find_function (str : string) : ResolveJSFunctionResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the use cases where user wants to call something on global namespace ?
Could we ask the user to pass globalThis
.foo.bar() explicitly themselves ?
What are the other options we have ?
If the target was function in es6 module, how could they pass the instance wrapped somewhere deep in webpacked code ?
We already have JSObject.Invoke
, do we need this one too ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make globalThis mandatory, yes. Good idea.
I think JSObject.Invoke needs to be separate (but can be implemented through this), because it invokes on a this-reference (dynamic) and can throw.
At one point we had an invoke icall with a generic
I could sit down and try to carefully enumerate why I think we need a new icall, but off the top of my head (this was a long design discussion that happened quite a while ago, so it's reasonable to revisit it):
My endgame for this is that we use source generators to create strongly-typed wrappers for specific methods (JS pinvoke, basically) that correctly and efficiently use this new icall under the hood. Later on those wrappers could transition to using dedicated JS and bypassing the icall machinery if necessary. |
790fb64
to
1023050
Compare
0011734
to
3c3555b
Compare
Now that I fixed the Iterator test, it's reliably broken with this PR applied. Still trying to figure out why. |
That aside, here are benchmark timings from an AOT run that just finished (the previous timings were for the interpreter):
|
3c3555b
to
36f6556
Compare
6d40718
to
dc95edb
Compare
Add test coverage for date marshaling Add uri test Disable some log statements Returning structs works Returning custom classes works Add null checks to describe_value Remove invalid cwrap Add trimming annotations Restore old benchmark html Reuse result and exception roots Cleaner codegen Disambiguate converters in devtools Transition back to static methods, add signature checks and more error handling Implement a simple bound method cache so that automated tests don't exhaust the scratch root buffer; make the scratch root buffer smaller Fix a LMF leak Fix C warnings Hard-coded table of marshalers is no longer needed Shorter wrapper function names for better stacks Add linker exclusions for custom marshalers Hack around bug in linker/runtime that causes System.Uri forwarder to break Rework class lookup Support passing numeric values as DateTime Transition some null returns to asserts Clean up conditionals Don't use ISO strings Change pre/post filter syntax to require an explicit "return" so it can contain multiple statements Allow ToJavaScript to accept a pointer instead of an 'in' reference Support managed pointer return/parameter types in more places Add marshal_type for pointers Add tests verifying that you can marshal structs by address and then unpack them Initial gc safety work Fix formatting rule Rename pre/post filters Fix type error in driver.c remove _pick_result_chara_for_marshal_type Add library build descriptor to ensure marshalers are not stripped when the BCL is trimmed Attempt to fix the linker stripping test code Better version of no-configured-marshaler warning Annotate SetupJSContinuation Annotate safehandle APIs and add null checks Update targets file to pass custom marshaler msbuild items through to the helix proxy project (requires another change to work) Add tests for Task and ValueTask marshaling Fix unboxing for generic structs Rebase cleanups Correct datetime test to use UTC for comparison Eliminate use of MONO. and BINDING. in closures Optimize out a js->c call Normalize some APIs to take MonoType instead of MonoClass Repair merge damage Address PR feedback Move some types around Repair merge damage Type system fixes Rework create_named_function so that it can handle larger numbers of closure keys more efficiently Remove unnecessary test instrumentation Fix closure variables being generated in the wrong place Use a single memory slab for temp_malloc Checkpoint span support Fix unbuffered filters, support ReadOnlySpan Fix auto signatures for primitives and add test Use 4-chara unicode escapes since the x escapes are not officially permitted in JSON Checkpoint C# implementation of converter generator Align everything by 8 when constructing argument buffers because if you don't do that, the runtime passes corrupt data to C# functions Don't shove raw mono pointers into root buffers since we weren't doing it before (it might be nice to do it though) Fully transition over to having C# generate signature converters C# implementation of bind_method codegen Clean up the bindings named closure table management Add some comments Don't allocate a root for the this-ref when binding methods if the this-ref is null
…gn C# and C's idea of marshal types better.
dc95edb
to
de3e4c1
Compare
Draft Pull Request was automatically closed for inactivity. Please let us know if you'd like to reopen it. |
This PR (which relies on the custom marshalers PR, #47640) introduces a new icall (
InvokeJSFunction
) that is used to invoke JS functions from C#. It takes a 'do it yourself' approach to things like error handling (in order to avoid JS roundtrips for try/catch blocks) and attempts to remove overhead in various other places so that scenarios not needing that overhead can be much faster. It's also designed to be (partially) AOT friendly by making the core icall not generic.The icall enforces some rules to produce good performance:
(RuntimeTypeHandle type, IntPtr arg)
pairs, where (depending on type):type
is a pointer,arg
is the raw pointer value.type
is a ValueType,arg
is the address of the value.type
is a reference type),arg
is the address of the location of the object reference on the stack. This is so that if the GC relocates the object, the callee will see the new post-relocation address.The new icall offers some new features as part of the deal:
Module._free
) and the icall will walk the scope chain to find the target function - we can afford to do this since we cache the result.BINDING
,MONO
orINTERNAL
, the icall will look the function up inside the appropriate runtime API object instead of in the global scope, so those function names will work even if we are running modularized and they are not exposed in the global scope.For scenarios not covered by the new icall, the intended solution is to expose higher-level convenience wrappers that provide the bells and whistles people expect. This keeps the icall slim and fast so that in use cases where you want maximum performance, you can get it. At present the PR provides
InvokeJSResult InvokeJSFunctionByName<T1, T2, T3> (string internedFunctionName, ref T1 arg1, ref T2 arg2, ref T3 arg3)
as a convenience wrapper that automatically handles producing the type/address pairs for you.As a proof of concept, the PR changes
JSObject.Invoke
,JSObject.SetProperty
andJSObject.GetProperty
to go through the new icall. I have a feeling we don't actually want to do this, but it serves as a good example of how to use the icall and was also useful for flushing out bugs and getting a sense of its performance.