Skip to content

Conversation

@pavelsavara
Copy link
Member

@pavelsavara pavelsavara commented Nov 27, 2025

Motivation: make it more CoreCLR and NAOT friendly

This is just rough idea so far:

  • instead of finding the JSExport wrappers by FQN + reflection
  • capture Action<IntPtr> wrapper during registration and add that into dictionary on the managed side
  • because all of them have the same native signature void (void* args) - we can use single [UMCO] to dispatch the calls from JS side
  • we can remove some string FQNs and [DynamicDependency] attributes from the generated code

Open problems so far

  • this adds Action<IntPtr> public API JSFunctionBinding.BindManagedFunction - will need API change
  • JSExports (in nugets) created by Roslyn in Net10 and below are not compatible (breaking change)
  • JSExports would not be supported on private classes anymore, because the C# registration code can't touch the generated wrapper. internal classes work just fine.
  • Mono invocation in invoke_async_jsexport2 is bit of a hack, but consolidation would solve that eventually

@pavelsavara pavelsavara added this to the 11.0.0 milestone Nov 27, 2025
@pavelsavara pavelsavara requested a review from maraf November 27, 2025 17:44
@pavelsavara pavelsavara self-assigned this Nov 27, 2025
@pavelsavara pavelsavara added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Nov 27, 2025
@pavelsavara
Copy link
Member Author

pavelsavara commented Nov 27, 2025

This is the new generated code (simplified)

namespace System.Runtime.InteropServices.JavaScript
{
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
    unsafe class __GeneratedInitializer
    {
        static void __Register_()
        {
            if (initialized || RuntimeInformation.OSArchitecture != Architecture.Wasm)
                return;
            initialized = true;
            JSFunctionBinding.BindManagedFunction(Sample.Test.__Wrapper_PrintMeaning_451547442, "PrintMeaning", 
                451547442, [JSMarshalerType.Task(), JSMarshalerType.Task(JSMarshalerType.Int32)]);
        }
    }
}
namespace Sample
{
    public unsafe partial class Test
    {
        [DebuggerNonUserCode]
        public static unsafe void __Wrapper_PrintMeaning_451547442(global::System.IntPtr __arguments_ptr)
        {
            JSMarshalerArgument* __arguments_buffer = (JSMarshalerArgument*)__arguments_ptr;
            __Stub(__arguments_buffer[2], __arguments_buffer, __arguments_buffer + 1);
            [DebuggerNonUserCode]
            void __Stub(JSMarshalerArgument __meaningPromise_native, JSMarshalerArgument* ____arg_exception_native__param, JSMarshalerArgument* __invokeRetValUnmanaged__param)
            {
               // cut for brevity
            }
        }
    }
}

@pavelsavara

This comment was marked as resolved.

@maraf
Copy link
Member

maraf commented Nov 28, 2025

JSExports would not be supported on private classes anymore, because the C# registration code can't touch the generated wrapper. internal classes work just fine.

Would the unsafe access help with it?

@pavelsavara
Copy link
Member Author

JSExports would not be supported on private classes anymore, because the C# registration code can't touch the generated wrapper. internal classes work just fine.

Would the unsafe access help with it?

[UnsafeAccessor] also need the first argument to be public type.

Alternatively, now that I moved the legacy reflection to managed code, that path still could be used for this scenario. We could keep generating reflection based code for private.

But both ideas feel like overkill to me. So far, I think expecting it to be internal is acceptable breaking change.

@jkotas
Copy link
Member

jkotas commented Nov 28, 2025

[UnsafeAccessor] also need the first argument to be public type.

It does not need to be a public type if you use UnsafeAccessorTypeAttribute. (I am not sure whether it helps with the problem you are trying to solve here.)

@pavelsavara
Copy link
Member Author

It does not need to be a public type if you use UnsafeAccessorTypeAttribute.

Thank you, I didn't know it existed!
Is that resolved at compile time into type/method metadata token ?
If that's just string type name that is resolved at runtime, it would not improve on the existing approach with FQN + reflection.

@jkotas
Copy link
Member

jkotas commented Nov 28, 2025

Is that resolved at compile time into type/method metadata token ?

It is resolved when the UnsafeAccessor is JITed or AOT compiled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-System.Runtime.InteropServices.JavaScript os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants