Skip to content

[Wasm] Write barriers#128225

Draft
kg wants to merge 4 commits into
dotnet:mainfrom
kg:wasm-writebarriers-callconv
Draft

[Wasm] Write barriers#128225
kg wants to merge 4 commits into
dotnet:mainfrom
kg:wasm-writebarriers-callconv

Conversation

@kg
Copy link
Copy Markdown
Member

@kg kg commented May 14, 2026

Sufficient for the R2R'd version of this to run without crashing:

    static string? TestField;

    [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
    public static void ConcatTwoStrings (string a, string b) {
        TestField = a + b;
    }

    [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
    public static int Main () {
        Console.WriteLine("Hello world!");
        ConcatTwoStrings("A", "B");
        Console.WriteLine(TestField);
        return 42;
    }

Currently the write barrier in ConcatTwoStrings fails due to the calling convention being wrong and then once the calling convention is fixed, it fails because we didn't implement the write barriers yet.

This PR:

  • Updates the calling convention of the write barriers to match that expected by the JIT (bare calling convention without sp/pep args) by adding new FCDECL2_RAW / FCIMPL2_RAW macros
  • Fixes RhpByRefAssignRef's signature being incorrect (missing *)
  • Implements C++ versions of the byref assign helpers for TARGET_WASM
  • Enables our C++ write barriers for TARGET_WASM (we were defining FEATURE_USE_ASM_GC_WRITE_BARRIERS before)

Copilot AI review requested due to automatic review settings May 14, 2026 20:11
@kg kg added the arch-wasm WebAssembly architecture label May 14, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor

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

Initial scaffolding to make the WASM portable write barriers (RhpAssignRef, RhpCheckedAssignRef, RhpByRefAssignRef) use a "raw" calling convention that matches what RyuJIT expects for write barriers, rather than the trampolined WASM FCALL convention that threads callersStackPointer / portableEntryPointContext through. The actual barrier implementations still assert and will be filled in later.

Changes:

  • Add new FCDECL2_RAW / FCIMPL2_RAW macros that bypass the WASM stack-pointer/entrypoint-context plumbing and define the function with the plain (a1, a2) signature; provide non-WASM fallbacks that alias to FCDECL2 / FCIMPL2.
  • Switch the three Rhp*AssignRef declarations in jitinterface.h and definitions in WriteBarriers.cpp to the new _RAW variants.
  • Trivial whitespace cleanup in jitinterface.h.

Reviewed changes

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

File Description
src/coreclr/vm/fcall.h Adds WASM-specific FCDECL2_RAW / FCIMPL2_RAW macros and non-WASM fallbacks aliasing to the standard FCDECL2 / FCIMPL2.
src/coreclr/vm/jitinterface.h Switches Rhp{Checked,ByRef,}AssignRef declarations to FCDECL2_RAW; trims trailing whitespace.
src/coreclr/runtime/portable/WriteBarriers.cpp Updates the three portable write-barrier stubs to use FCDECL2_RAW / FCIMPL2_RAW.

Copy link
Copy Markdown
Contributor

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

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

Comment thread src/coreclr/vm/fcall.h
#define FCDECL1(rettype, funcname, a1) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext); rettype F_CALL_CONV funcname ## _IMPL(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext)
#define FCDECL1_V(rettype, funcname, a1) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext); rettype F_CALL_CONV funcname ## _IMPL(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext)
#define FCDECL2(rettype, funcname, a1, a2) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, a2, PCODE portableEntryPointContext); rettype F_CALL_CONV funcname ## _IMPL(uintptr_t callersStackPointer, a1, a2, PCODE portableEntryPointContext)
#define FCDECL2_RAW(rettype, funcname, a1, a2) rettype F_CALL_CONV funcname(a1, a2); rettype F_CALL_CONV funcname(a1, a2)
Comment on lines +15 to +42
#ifdef TARGET_WASM
*dst = ref;
JIT_WriteBarrier(dst, ref);
#else
PORTABILITY_ASSERT("RhpAssignRef is not yet implemented");
#endif // TARGET_WASM
}
FCIMPLEND

EXTERN_C FCDECL2(VOID, RhpCheckedAssignRef, Object **dst, Object *ref);
FCIMPL2(VOID, RhpCheckedAssignRef, Object **dst, Object *ref)
EXTERN_C FCDECL2_RAW(VOID, RhpCheckedAssignRef, Object **dst, Object *ref);
FCIMPL2_RAW(VOID, RhpCheckedAssignRef, Object **dst, Object *ref)
{
#ifdef TARGET_WASM
*dst = ref;
JIT_CheckedWriteBarrier(dst, ref);
#else
PORTABILITY_ASSERT("RhpCheckedAssignRef is not yet implemented");
#endif // TARGET_WASM
}
FCIMPLEND

EXTERN_C FCDECL2(VOID, RhpByRefAssignRef, Object **dst, Object *ref);
FCIMPL2(VOID, RhpByRefAssignRef, Object **dst, Object *ref)
EXTERN_C FCDECL2_RAW(VOID, RhpByRefAssignRef, Object **dst, Object **ref);
FCIMPL2_RAW(VOID, RhpByRefAssignRef, Object **dst, Object **ref)
{
#ifdef TARGET_WASM
Object *tmp = *ref;
*dst = tmp;
JIT_CheckedWriteBarrier(dst, tmp);
{
#ifdef TARGET_WASM
*dst = ref;
JIT_WriteBarrier(dst, ref);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should just move the implementation to here, and delete it where it is currently. We do not want to pay for the extra call.

EXTERN_C FCDECL2_RAW(VOID, RhpAssignRef, Object **dst, Object *ref);
FCIMPL2_RAW(VOID, RhpAssignRef, Object **dst, Object *ref)
{
#ifdef TARGET_WASM
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These TARGET_WASM ifdefs should not be needed.

Comment thread src/coreclr/vm/fcall.h
#define HCIMPL1_RAW(rettype, funcname, a1) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext) {
#define HCIMPL1_V(rettype, funcname, a1) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, PCODE portableEntryPointContext) { HCIMPL_PROLOG(funcname)
#define HCIMPL2(rettype, funcname, a1, a2) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, a2, PCODE portableEntryPointContext) { HCIMPL_PROLOG(funcname)
#define HCIMPL2_RAW(rettype, funcname, a1, a2) rettype F_CALL_CONV funcname(uintptr_t callersStackPointer, a1, a2, PCODE portableEntryPointContext) {
Copy link
Copy Markdown
Member

@jkotas jkotas May 14, 2026

Choose a reason for hiding this comment

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

I do not think you can drop callersStackPointer. It would be assuming that the C/C++ compiler does not spill anything to the stack. It is very fragile assumption.

If we wanted to drop callersStackPointer arg, we would have to implement these in wasm assembly to make sure that nothing is spilled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we wanted to drop callersStackPointer arg, we would have to implement these in wasm assembly to make sure that nothing is spilled.

That is how I would expect these to be implemented. Inserting the SP argument is somewhat complex for the Jit in case of barriers, and it's worse CQ of course.

Comment on lines +40 to +42
Object *tmp = *ref;
*dst = tmp;
JIT_CheckedWriteBarrier(dst, tmp);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Object *tmp = *ref;
*dst = tmp;
JIT_CheckedWriteBarrier(dst, tmp);
JIT_CheckedWriteBarrier(dst, *ref);

JIT_CheckedWriteBarrier does the ref->dst assignment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Interesting, thank you. I guess that's a difference between our WB and the nativeaot one.

FCIMPL2_RAW(VOID, RhpAssignRef, Object **dst, Object *ref)
{
#ifdef TARGET_WASM
*dst = ref;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*dst = ref;

FCIMPL2_RAW(VOID, RhpCheckedAssignRef, Object **dst, Object *ref)
{
#ifdef TARGET_WASM
*dst = ref;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
*dst = ref;

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

Labels

arch-wasm WebAssembly architecture area-VM-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants