// 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.CallingConvention;
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');
private bool HasGenericContextBeforeAsync
{
get
{
int asyncMarkerIndex = _wasmSignature.SignatureString.IndexOf('a');
if (asyncMarkerIndex < 0)
{
return false;
}
int pos = 1;
if (_wasmSignature.SignatureString[0] == 'S')
{
while ((pos < _wasmSignature.SignatureString.Length) && char.IsDigit(_wasmSignature.SignatureString[pos]))
{
pos++;
}
}
if ((pos < _wasmSignature.SignatureString.Length) && (_wasmSignature.SignatureString[pos] == 'T'))
{
pos++;
}
char hiddenParamChar = (_context.Target.PointerSize == 4) ? 'i' : 'l';
return (pos < asyncMarkerIndex) && (_wasmSignature.SignatureString[pos] == hiddenParamChar);
}
}
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);
MethodSignature methodSignature = WasmLowering.RaiseSignature(_wasmSignature, _context);
bool hasAsyncContinuation = HasAsyncContinuation;
bool hasGenericContextBeforeAsync = HasGenericContextBeforeAsync;
(ArgIterator<TypeHandle> argit, TransitionBlock transitionBlock) = GCRefMapBuilder.BuildArgIterator(methodSignature, _context, methodIsAsyncCall: hasAsyncContinuation);
bool hasRetBuffArg = _wasmSignature.SignatureString[0] == 'S';
bool hasThis = !methodSignature.IsStatic;
int[] offsets = new int[methodSignature.Length];
bool[] isIndirectStructArg = new bool[methodSignature.Length];
int argIndex = 0;
int argOffset;
while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset)
{
offsets[argIndex] = argOffset;
isIndirectStructArg[argIndex] = WasmLowering.CurrentArgLowersValueTypeToPassAsByref(argit);
argIndex++;
}
argit.Reset();
int sizeOfArgumentArray = argit.SizeOfFrameArgumentArray();
int sizeOfTransitionBlock = 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;
for (int i = 0; i < offsets.Length; i++)
{
offsets[i] += transitionBlockOffset;
}
int asyncContinuationOffset = hasAsyncContinuation ? argit.GetAsyncContinuationArgOffset() + transitionBlockOffset : 0;
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 wasmLocalIndex = 1; // local 0 is $sp
// Handle 'this' pointer — it occupies a wasm local but is not in methodSignature.Length
if (hasThis)
{
int thisOffset = transitionBlock.ThisOffset + transitionBlockOffset;
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(wasmLocalIndex));
expressions.Add(I32.Store((ulong)thisOffset));
wasmLocalIndex++;
}
// Hidden retbuf pointer occupies a wasm local but is not in methodSignature params
if (hasRetBuffArg)
{
wasmLocalIndex++;
}
if (hasAsyncContinuation && !hasGenericContextBeforeAsync)
{
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(wasmLocalIndex));
expressions.Add(I32.Store((ulong)asyncContinuationOffset));
wasmLocalIndex++;
}
for (int i = 0; i < methodSignature.Length; i++)
{
TypeDesc paramType = methodSignature[i];
int currentOffset = offsets[i];
if (WasmLowering.IsEmptyStruct(paramType))
{
expressions.Add(Local.Get(0));
expressions.Add(I32.Const(0));
expressions.Add(I32.Store((ulong)currentOffset));
}
else if (WasmLowering.TryGetMultiSegmentLayout(paramType, out WasmValueType slotType, out int slotCount))
{
// Passed by value across several wasm locals — store each one into the argument area.
int slotSize = WasmLowering.GetMultiSegmentSlotSize(slotType);
for (int slot = 0; slot < slotCount; slot++)
{
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(wasmLocalIndex));
ulong slotOffset = (ulong)(currentOffset + (slot * slotSize));
expressions.Add(slotType == WasmValueType.I64 ? I64.Store(slotOffset) : V128.Store(slotOffset));
wasmLocalIndex++;
}
}
else if (isIndirectStructArg[i])
{
// Indirect struct — copy the exact contents from the incoming pointer
int structSize = paramType.GetElementSize().AsInt;
// 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(wasmLocalIndex));
// 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());
}
wasmLocalIndex++;
}
else
{
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(wasmLocalIndex));
WasmValueType type = _typeNode.Type.Params.Types[wasmLocalIndex];
switch (type)
{
case WasmValueType.I32:
expressions.Add(I32.Store((ulong)currentOffset));
break;
case WasmValueType.F32:
expressions.Add(F32.Store((ulong)currentOffset));
break;
case WasmValueType.I64:
expressions.Add(I64.Store((ulong)currentOffset));
break;
case WasmValueType.F64:
expressions.Add(F64.Store((ulong)currentOffset));
break;
case WasmValueType.V128:
expressions.Add(V128.Store((ulong)currentOffset));
break;
default:
throw new Exception("Unexpected wasm type arg");
}
wasmLocalIndex++;
}
if (hasAsyncContinuation && hasGenericContextBeforeAsync && (i == 0))
{
expressions.Add(Local.Get(0));
expressions.Add(Local.Get(wasmLocalIndex));
expressions.Add(I32.Store((ulong)asyncContinuationOffset));
wasmLocalIndex++;
}
}
// 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 (last wasm local)
int portableEntrypointLocalIndex = _typeNode.Type.Params.Types.Length - 1;
expressions.Add(Local.Get(portableEntrypointLocalIndex));
// 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 (hasRetBuffArg)
{
// The retbuf is a wasm parameter — pass it through directly.
// For managed calls: local 0 = $sp, local 1 = this (if present), then retbuf.
int retBufLocalIndex = 1 + (hasThis ? 1 : 0);
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));
switch (returnWasmType)
{
case WasmValueType.I32:
expressions.Add(I32.Load((ulong)localRetBufOffset));
break;
case WasmValueType.F32:
expressions.Add(F32.Load((ulong)localRetBufOffset));
break;
case WasmValueType.I64:
expressions.Add(I64.Load((ulong)localRetBufOffset));
break;
case WasmValueType.F64:
expressions.Add(F64.Load((ulong)localRetBufOffset));
break;
case WasmValueType.V128:
expressions.Add(V128.Load((ulong)localRetBufOffset));
break;
default:
throw new Exception("Unexpected wasm return type");
}
}
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(); }
}
}