// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using ILCompiler.DependencyAnalysis.Wasm;
using ILCompiler.ObjectWriter;
using ILCompiler.ObjectWriter.WasmInstructions;
using Internal.JitInterface;
using Internal.Text;
using Internal.TypeSystem;
using Internal.ReadyToRunConstants;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using ILCompiler.DependencyAnalysisFramework;
namespace ILCompiler.DependencyAnalysis.ReadyToRun
{
/// <summary>
/// A thunk that captures all arguments and dispatches to the interpreter via
/// READYTORUN_HELPER_R2RToInterpreter. This node is string-discoverable so the
/// runtime can find it by WasmSignature string at execution time.
/// </summary>
public class WasmR2RToInterpreterThunkNode : StringDiscoverableAssemblyStubNode, INodeWithTypeSignature, ISymbolDefinitionNode, ISortableSymbolNode
{
private readonly TypeSystemContext _context;
private readonly WasmSignature _wasmSignature;
private readonly WasmTypeNode _typeNode;
private readonly Import _helperCell;
// Helper signature: (I32, I32, I32, I32) -> ()
private static readonly CorInfoWasmType[] s_helperTypeParams = new CorInfoWasmType[]
{
CorInfoWasmType.CORINFO_WASM_TYPE_VOID,
CorInfoWasmType.CORINFO_WASM_TYPE_I32,
CorInfoWasmType.CORINFO_WASM_TYPE_I32,
CorInfoWasmType.CORINFO_WASM_TYPE_I32,
CorInfoWasmType.CORINFO_WASM_TYPE_I32,
};
public override bool StaticDependenciesAreComputed => true;
public override bool IsShareable => false;
public override ObjectNodeSection GetSection(NodeFactory factory) => ObjectNodeSection.TextSection;
public override string LookupString => "I" + _wasmSignature.SignatureString;
MethodSignature INodeWithTypeSignature.Signature => WasmLowering.RaiseSignature(_wasmSignature, _context);
bool INodeWithTypeSignature.IsUnmanagedCallersOnly => false;
bool INodeWithTypeSignature.IsAsyncCall => HasAsyncContinuation;
bool INodeWithTypeSignature.HasGenericContextArg => false;
private bool HasAsyncContinuation => _wasmSignature.SignatureString.Contains('a');
public WasmR2RToInterpreterThunkNode(NodeFactory factory, WasmSignature wasmSignature)
{
_context = factory.TypeSystemContext;
_wasmSignature = wasmSignature;
_typeNode = factory.WasmTypeNode(wasmSignature);
_helperCell = factory.GetReadyToRunHelperCell(ReadyToRunHelper.R2RToInterpreter);
}
public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
{
sb.Append("WasmR2RToInterpreterThunk("u8);
sb.Append(_wasmSignature.SignatureString);
sb.Append(")"u8);
}
protected override string GetName(NodeFactory factory)
{
Utf8StringBuilder sb = new Utf8StringBuilder();
AppendMangledName(factory.NameMangler, sb);
return sb.ToString();
}
public override int ClassCode => 948271449;
public override int CompareToImpl(ISortableNode other, CompilerComparer comparer)
{
WasmR2RToInterpreterThunkNode otherNode = (WasmR2RToInterpreterThunkNode)other;
return _wasmSignature.CompareTo(otherNode._wasmSignature);
}
protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory)
{
DependencyList dependencies = base.ComputeNonRelocationBasedDependencies(factory);
dependencies.Add(_typeNode, "Wasm R2R to interpreter thunk requires type node");
return dependencies;
}
protected override void EmitCode(NodeFactory factory, ref Wasm.WasmEmitter instructionEncoder, bool relocsOnly)
{
Debug.Assert(!instructionEncoder.Is64Bit);
ISymbolNode helperTypeIndex = factory.WasmTypeNode(s_helperTypeParams);
WasmThunkArgLayout layout = new WasmThunkArgLayout(_wasmSignature, _context);
int sizeOfArgumentArray = layout.SizeOfFrameArgumentArray;
int sizeOfTransitionBlock = layout.TransitionBlock.SizeOfTransitionBlock;
// The arguments area must be 16-byte aligned. The TransitionBlock (8 bytes on Wasm32)
// sits before the arguments, so it is 8-byte aligned but not 16-byte aligned.
// Layout from base: [TransitionBlock (8)] [args...]
int argumentsOffset = AlignmentHelper.AlignUp(sizeOfTransitionBlock, 16);
int transitionBlockOffset = argumentsOffset - sizeOfTransitionBlock;
int sizeOfStoredLocals = argumentsOffset + AlignmentHelper.AlignUp(sizeOfArgumentArray, 16);
bool hasWasmReturn = _typeNode.Type.Returns.Types.Length > 0;
WasmValueType returnWasmType = hasWasmReturn ? _typeNode.Type.Returns.Types[0] : default;
// Allocate a local return buffer. V128 needs 16 bytes and 16-byte alignment;
// other types need at most 8 bytes. localRetBufOffset is already 16-byte aligned
// (argumentsOffset is 16-aligned, and args size is rounded up to 16).
int retBufSize = (hasWasmReturn && returnWasmType == WasmValueType.V128) ? 16 : 8;
int localRetBufOffset = sizeOfStoredLocals;
int totalAlloc = AlignmentHelper.AlignUp(sizeOfStoredLocals + retBufSize, 16);
List<WasmExpr> expressions = new List<WasmExpr>();
// Allocate stack space: local.get 0; i32.const totalAlloc; i32.sub; local.set 0
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(totalAlloc));
expressions.Add(I32.Sub);
expressions.Add(Local.Set(0));
// Initialize TransitionBlock:
// First 4 bytes (m_ReturnAddress) = 0
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(0));
expressions.Add(I32.Store((ulong)transitionBlockOffset));
// Second 4 bytes (m_StackPointer) = original SP (local 0 + totalAlloc)
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(totalAlloc));
expressions.Add(I32.Add);
expressions.Add(I32.Store((ulong)(transitionBlockOffset + 4)));
// Store all arguments into the transition block area
int? retBufParamIndex = null;
foreach (WasmThunkArg arg in layout.Args)
{
if (arg.Kind == WasmThunkArgKind.RetBuf)
{
retBufParamIndex = arg.WasmParamIndex;
continue;
}
int currentOffset = arg.Offset + transitionBlockOffset;
if (arg.IsEmptyStruct)
{
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(0));
expressions.Add(I32.Store((ulong)currentOffset));
}
else if (arg.IsIndirectStruct)
{
// Indirect struct — copy the exact contents from the incoming pointer
int structSize = arg.IndirectStructSize;
// memory.copy: (dst, src, len) -> ()
// dst: base + currentOffset
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(currentOffset));
expressions.Add(I32.Add);
// src: the byref pointer passed as the wasm local
expressions.Add(Local.Get(arg.WasmParamIndex));
// len: struct size
expressions.Add(I32.Const(structSize));
expressions.Add(Memory.Copy());
// Pad remaining bytes to alignment boundary with zeros
int alignment = structSize <= 4 ? 4 : 8;
int padding = AlignmentHelper.AlignUp(structSize, alignment) - structSize;
if (padding > 0)
{
// memory.fill: (dst, val, len) -> ()
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(currentOffset + structSize));
expressions.Add(I32.Add);
expressions.Add(I32.Const(0));
expressions.Add(I32.Const(padding));
expressions.Add(Memory.Fill());
}
}
else
{
// Scalars, hidden arguments, and each slot of a multi-slot value.
int slotSize = arg.IsMultiSlot ? WasmLowering.GetMultiSegmentSlotSize(arg.WasmType) : 0;
for (int slot = 0; slot < arg.WasmParamCount; slot++)
{
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(arg.WasmParamIndex + slot));
expressions.Add(Memory.Store(arg.WasmType, (ulong)(currentOffset + (slot * slotSize))));
}
}
}
// Zero the local return buffer
if (retBufSize <= 8)
{
expressions.Add(Local.Get(0));
expressions.Add(I64.Const(0));
expressions.Add(I64.Store((ulong)localRetBufOffset));
}
else
{
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(localRetBufOffset));
expressions.Add(I32.Add);
expressions.Add(I32.Const(0));
expressions.Add(I32.Const(retBufSize));
expressions.Add(Memory.Fill());
}
// Prepare helper call arguments:
// arg1: portable entrypoint
expressions.Add(Local.Get(layout.PortableEntrypointParamIndex));
// arg2: pointer to the collected arguments and transition block (base + transitionBlockOffset)
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(transitionBlockOffset));
expressions.Add(I32.Add);
// arg3: size of arguments (excluding transition block)
expressions.Add(I32.Const(sizeOfArgumentArray));
// arg4: return buffer pointer
if (retBufParamIndex is int retBufLocalIndex)
{
// The retbuf is a wasm parameter — pass it through directly.
expressions.Add(Local.Get(retBufLocalIndex));
}
else
{
// Pass pointer to local 8-byte return buffer
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(localRetBufOffset));
expressions.Add(I32.Add);
}
// Extra local to save/restore the stack pointer global across the helper call
int savedSpLocalIndex = _typeNode.Type.Params.Types.Length;
// Save the current stack pointer global into a local, then set it to local 0
// (16-byte aligned, <= all buffers allocated in this thunk).
expressions.Add(Global.Get(WebCilObjectWriter.StackPointerGlobalIndex));
expressions.Add(Local.Set(savedSpLocalIndex));
expressions.Add(Local.Get(0));
expressions.Add(Global.Set(WebCilObjectWriter.StackPointerGlobalIndex));
// Load the helper function address and call
expressions.Add(Global.Get(WebCilObjectWriter.ImageBaseGlobalIndex));
expressions.Add(I32.LoadWithRVAOffset(_helperCell));
expressions.Add(ControlFlow.CallIndirect(helperTypeIndex, 0));
// The interpreter publishes its async continuation to the global itself
// (prestub.cpp), so there is nothing to propagate back here.
// Restore the old stack pointer global
expressions.Add(Local.Get(savedSpLocalIndex));
expressions.Add(Global.Set(WebCilObjectWriter.StackPointerGlobalIndex));
// If the function has a wasm return value, load it from the local return buffer
if (hasWasmReturn)
{
Debug.Assert(_typeNode.Type.Returns.Types.Length == 1, "Expected exactly one wasm return type");
expressions.Add(Local.Get(0));
expressions.Add(Memory.Load(returnWasmType, (ulong)localRetBufOffset));
}
instructionEncoder.FunctionBody = new WasmFunctionBody(_typeNode.Type, new[] { WasmValueType.I32 }, expressions.ToArray());
}
protected override void EmitCode(NodeFactory factory, ref X64.X64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
protected override void EmitCode(NodeFactory factory, ref X86.X86Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
protected override void EmitCode(NodeFactory factory, ref ARM.ARMEmitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
protected override void EmitCode(NodeFactory factory, ref ARM64.ARM64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
protected override void EmitCode(NodeFactory factory, ref LoongArch64.LoongArch64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
protected override void EmitCode(NodeFactory factory, ref RiscV64.RiscV64Emitter instructionEncoder, bool relocsOnly) { throw new NotSupportedException(); }
}
}