Skip to content

Conversation

AaronRobinsonMSFT
Copy link
Member

The broken scenario was that value types are passed by value if they are <= sizeof(void*), but pass by reference if > sizeof(void*). This needed to be reconciled with the current interpreter ABI that passes values types on the stack.

The broken scenario was that value types are passed
by value if they are <= sizeof(void*), but pass by
reference if > sizeof(void*). This needed to be reconciled
with the current interpreter ABI that passes values types
on the stack.
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @mangod9
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

pass by reference if > sizeof(void*)

I do not think that the rules are that simple.

Take a look at https://godbolt.org/z/8aTqWMEb3 . It shows that struct S { int x; } is passed by value as int32, but struct S2 { char a,b,c,d; }; is passed by reference. The type size is sizeof(void*) in both cases.

@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

@AaronRobinsonMSFT
Copy link
Member Author

I think, at least for now, the only additional nuance I should apply is the multi field aspect. I was searching for a nice clear spec, but it looks like chatgpt is about as helpful as can be.

@AaronRobinsonMSFT
Copy link
Member Author

The next failing issue is the following stack:

Frame (InterpreterFrame): 0x4fee78
   0) IGenericCacheEntry`1[__Canon]::CreateAndCache, IR_003d
   1) IGenericCacheEntry`1[__Canon]::GetOrCreate, IR_00b0
   2) System.RuntimeType::GetOrCreateCacheEntry, IR_000f
   3) System.RuntimeType::CreateInstanceOfT, IR_0029
   4) System.Activator::CreateInstance, IR_0033
   5) ManagedToUnmanagedOut[__Canon]::.ctor, IR_0019
   6) Sys::Dup, IR_0025
   7) System.ConsolePal::OpenStandardOutput, IR_0013
   8) System.Console::<get_Out>g__EnsureInitialized|26_0, IR_003b
   9) System.Console::get_Out, IR_003c
  10) System.Console::WriteLine, IR_0008
  11) Program::Main, IR_000b

The associated IR is below. This appears to be the static abstract, IGenericCacheEntry<TCache>::Create(RuntimeType type). The failure is that we are going down the managed function calli case and the PCODE doesn't contain native instructions or interpreter bytes.

IR_0009: call           [176 <- 176], .System.RuntimeType:get_Cache()
IR_000d: call           [160 <- 176], .System.RuntimeType+RuntimeTypeCache:get_GenericCache()
IR_0011: mov.4          [16 <- 160],
IR_0014: mov.4          [160 <- 16],
IR_0017: ldind.i4       [160 <- 160], 0
IR_001b: mov.4          [24 <- 160],
IR_001e: mov.4          [160 <- 24],
IR_0021: ldc.i4.0       [168 <- nil],
IR_0023: ceq.i4         [160 <- 160 168],
IR_0027: mov.4          [32 <- 160],
IR_002a: mov.i4.u1      [160 <- 32],
IR_002d: brfalse.i4     [nil <- 160], IR_0068
IR_0030: mov.4          [176 <- 8],
IR_0033: generic        [160 <- 0],Class,0x46ec36c[36,0,12]
IR_0037: calli          [160 <- 176 160],0xbfd   <-----------Failure
IR_003d: mov.4          [40 <- 160],
IR_0040: mov.4          [184 <- 16],
IR_0043: mov.4          [192 <- 40],
IR_0046: ldc.i4.0       [200 <- nil],

@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

The failure is that we are going down the managed function calli case and the PCODE doesn't contain native instructions or interpreter bytes.

Managed CALLI needs to do similar dance as regular call for FEATURE_PORTABLE_ENTRYPOINT: If there is no interpreter code attached to the PortableEntryPoint, call DoPrestub and stay in the interpreter loop if the interpreter code shows up.

@AaronRobinsonMSFT
Copy link
Member Author

@jkotas Thanks. That let me move a bit farther, we're now hitting the following:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal object? CreateUninitializedObject(RuntimeType rt)
{
// We don't use RuntimeType, but we force the caller to pass it so
// that we can keep it alive on their behalf. Once the object is
// constructed, we no longer need the reference to the type instance,
// as the object itself will keep the type alive.
#if DEBUG
CheckOriginalRuntimeType(rt);
#endif
object? retVal = _pfnAllocator(_allocatorFirstArg);
GC.KeepAlive(rt);
return retVal;
}

The particular issue here is _pfnAllocator is an unwrapped PCODE (that is, not a PortableEntryPoint). I'm guessing we need to update the following location to wrap the PCODE in a new PortableEntryPoint instance. I would have assumed that GetMultiCallableAddrOfCode() created a PortableEntryPoint, but perhaps something is amiss there.

*ppfnAllocator = CoreLibBinder::GetMethod(METHOD__RT_TYPE_HANDLE__ALLOCATECOMOBJECT)->GetMultiCallableAddrOfCode();

@AaronRobinsonMSFT
Copy link
Member Author

Oops. Looks like we went down the following path:

// managed sig: MethodTable* -> object (via JIT helper)
bool fHasSideEffectsUnused;
*ppfnAllocator = CEEJitInfo::getHelperFtnStatic(CEEInfo::getNewHelperStatic(pMT, &fHasSideEffectsUnused));

Looks like we will need to update CEEJitInfo::getHelperFtnStatic().

@radekdoulik
Copy link
Member

radekdoulik commented Sep 10, 2025

Great progress. I think this is the spec/convention you were looking for https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#function-arguments-and-return-values

@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

Looks like we will need to update CEEJitInfo::getHelperFtnStatic()

Yes, looks like it. getHelperFtn may want to just call getHelperFtnStatic for FEATURE_PORTABLE_ENTRYPOINTS, and the wrapping and caching logic may want to move there.

radekdoulik and others added 5 commits September 10, 2025 13:41
signature: void *(valuetype System.Runtime.CompilerServices.QCallTypeHandle,valuetype System.TypeNameFormatFlags,valuetype System.Runtime.CompilerServices.StringHandleOnStack)
signature: valuetype System.RuntimeMethodHandleInternal *(valuetype System.Runtime.CompilerServices.QCallModule,int32,native int*,int32,native int*,int32)
@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

RunClassConstructor
ConstructName
GetCustomAttributeProps
ResolveType
ResolveMethod

Just curious - where are all these calls coming from?

@radekdoulik
Copy link
Member

RunClassConstructor
ConstructName
GetCustomAttributeProps
ResolveType
ResolveMethod

Just curious - where are all these calls coming from?

Good question. I have got a bit carried away and didn't look closely.

We are getting to the first one from DllNotFound exception. I will look at it after meetings.

ASSERT FAILED
        Expression: cookie != NULL
        Location:   line 402 in /Users/rodo/git/runtime-wasm-coreclr/src/coreclr/vm/wasm/helpers.cpp
        Function:   InvokeUnmanagedCalli
        Process:    42
Frame (InlinedCallFrame): 0x4fabbc
    Skipping 0x4fabbc
Frame (InterpreterFrame): 0x4fb7c8
   0) dynamicClass::IL_STUB_PInvoke, IR_0023
   1) System.Runtime.CompilerServices.RuntimeHelpers::RunClassConstructor, IR_0034
   2) System.SR::InternalGetResourceString, IR_014d
   3) System.SR::GetResourceString, IR_0025
   4) System.SR::get_Arg_DllNotFoundException, IR_000b
   5) System.DllNotFoundException::.ctor, IR_000b
Frame (InterpreterFrame): 0x4fcdf8
   6) System.Runtime.InteropServices.Marshal::SetLastSystemError, IR_000b
   7) Kernel32::GetEnvironmentVariable, IR_002d
   8) System.Environment::GetEnvironmentVariableCore, IR_0042
   9) System.Environment::GetEnvironmentVariable, IR_0015
  10) System.Runtime.InteropServices.SafeHandle::.cctor, IR_000b
Frame (InlinedCallFrame): 0x4fe05c
    Skipping 0x4fe05c
Frame (InterpreterFrame): 0x4fee28
  11) dynamicClass::IL_STUB_PInvoke, IR_0020
  12) System.Runtime.CompilerServices.InitHelpers::InitClassSlow, IR_000b
  13) System.Runtime.CompilerServices.InitHelpers::InitClass, IR_0021
  14) System.Runtime.InteropServices.SafeHandle::.ctor, IR_004a
  15) Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid::.ctor, IR_0013
  16) Microsoft.Win32.SafeHandles.SafeFileHandle::.ctor, IR_0027
  17) Microsoft.Win32.SafeHandles.SafeFileHandle::.ctor, IR_0011
  18) ActivatorCache::CallRefConstructor, IR_001a
  19) System.RuntimeType::CreateInstanceOfT, IR_006e
  20) System.Activator::CreateInstance, IR_0033
  21) ManagedToUnmanagedOut[__Canon]::.ctor, IR_0019
  22) Sys::Dup, IR_0025
  23) System.ConsolePal::OpenStandardOutput, IR_0013
  24) System.Console::<get_Out>g__EnsureInitialized|26_0, IR_003b
  25) System.Console::get_Out, IR_003c
  26) System.Console::WriteLine, IR_0008
  27) Program::<Main>$, IR_000b

@AaronRobinsonMSFT
Copy link
Member Author

We are getting to the first one from DllNotFound exception. I will look at it after meetings.

This is because we are trying to load libSystem.Native. However, since we are targeting WASM and not Apple, the .so suffix is being used - see src/coreclr/pal/inc/pal.h and the PAL_SHLIB_SUFFIX_W macro definitions.

@jkotas
Copy link
Member

jkotas commented Sep 10, 2025

We are getting to the first one from DllNotFoundException. I will look at it after meetings.

It is not interesting to work on fixing DllNotFoundException throwing path at this point. You will eventually hit exception handling that will quite a bit of work to bring up that's outside the scope of this PR.

Try to fix whatever is causing DllNotFoundException to be thrown instead.

@radekdoulik
Copy link
Member

Yes, I will check whether we already need libSystem.Native and if so, I will try to link libSystem.Native to development hosts and check how to start calling into that.

@jkotas jkotas marked this pull request as ready for review September 11, 2025 02:06
@jkotas jkotas requested review from Copilot and removed request for BrzVlad, janvorli and kg September 11, 2025 02:06
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes WASM logic for correctly locating stubs when handling value types in the interpreter ABI. The issue was reconciling how value types are passed: by value if they are <= sizeof(void*), but by reference if > sizeof(void*), with the current interpreter ABI that passes value types on the stack.

Key changes:

  • Enhanced argument conversion logic to support indirect argument passing for large value types
  • Added new thunk functions for handling indirect argument signatures
  • Refactored helper function resolution to improve code organization

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/coreclr/vm/wasm/helpers.cpp Added new thunk functions for indirect arguments and enhanced argument type conversion logic to handle value types based on size and field count
src/coreclr/vm/jitinterface.cpp Refactored helper function resolution by extracting common logic into getHelperFtnStatic and renaming parameter for clarity
src/coreclr/vm/interpexec.cpp Added portable entry point handling for WASM to properly route calls between interpreter and native code

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM

@radekdoulik @pavelsavara Feel free to merge this if it looks good to you as well.

@radekdoulik radekdoulik merged commit b0b30e7 into dotnet:main Sep 11, 2025
96 of 98 checks passed
@AaronRobinsonMSFT AaronRobinsonMSFT deleted the callstub_lookup branch September 11, 2025 15:26
@github-actions github-actions bot locked and limited conversation to collaborators Oct 12, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasm WebAssembly architecture area-VM-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants