| File: PortableCallHelpers\InterpToNativeGenerator.cs | Web Access |
| Project: ILCompiler.ReadyToRun.csproj (ILCompiler.ReadyToRun) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Internal.JitInterface; using Internal.TypeSystem; namespace ILCompiler.PortableCallHelpers { /// <summary> /// Generates the <c>g_portableCallHelperThunks</c> array and <c>CallFunc_*</c> functions used by the CoreCLR /// interpreter to call native code on wasm. /// </summary> /// <remarks> /// The generated code has to stay in sync with the CoreCLR runtime code that consumes these /// thunks and call functions. /// </remarks> internal static class InterpToNativeGenerator { public static void Emit(TextWriter w, IReadOnlyDictionary<string, MethodDesc> cookies) { string[] signatures = cookies.Keys.ToArray(); Array.Sort(signatures, StringComparer.Ordinal); // Collect unique struct return sizes so we can emit typedefs var structReturnSizes = new SortedSet<int>(); foreach (string signature in signatures) { string returnToken = InteropSignature.ParseSignatureTokens(signature)[0]; if (returnToken[0] == 'S' && returnToken.Length > 1) structReturnSizes.Add(InteropSignature.GetStructSize(returnToken)); } w.Write( """ // // GENERATED FILE, DON'T EDIT // Generated by coreclr InterpToNativeGenerator // #include <callhelpers.hpp> #include <minipal/utils.h> // Arguments are passed on the stack with each argument aligned to INTERP_STACK_SLOT_SIZE. #define ARG_ADDR(i) (pArgs + (i * INTERP_STACK_SLOT_SIZE)) #define ARG_IND(i) ((int32_t)((int32_t*)ARG_ADDR(i))) #define ARG_I32(i) (*(int32_t*)ARG_ADDR(i)) #define ARG_I64(i) (*(int64_t*)ARG_ADDR(i)) #define ARG_F32(i) (*(float*)ARG_ADDR(i)) #define ARG_F64(i) (*(double*)ARG_ADDR(i)) """); // Emit typedefs for struct return types so emcc generates the correct sret ABI foreach (int size in structReturnSizes) w.WriteLine($"typedef struct {{ char d[{size}]; }} portable_callhelper_ret_S{size};"); w.Write( """ namespace { """); foreach (string signature in signatures) { try { List<string> tokens = InteropSignature.ParseSignatureTokens(signature); string returnToken = tokens[0]; (bool isVoid, string nativeType) result = Result(returnToken); bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens); if (isPortableEntryPointCall) { // Portable entrypoints have an extra hidden parameter for the portable entrypoint // context, so adjust the signature and result accordingly for the call function. tokens.RemoveAt(tokens.Count - 1); } RemoveAsyncCallMarker(tokens); List<string> args = Args(tokens); string argTypes = string.Join(", ", args.Select(InteropSignature.TokenToNativeType)); string portableEntryPointComma = args.Count > 0 ? ", " : ""; string portableEntrypointDeclaration = isPortableEntryPointCall ? portableEntryPointComma + "PCODE" : ""; string portableEntrypointParam = isPortableEntryPointCall ? portableEntryPointComma + "pPortableEntryPoint" : ""; string portableEntrypointStackDeclaration = isPortableEntryPointCall ? "int*, " : ""; string portableEntrypointStackParam = isPortableEntryPointCall ? "&framePointer, " : ""; string portableEntrypointPointerRD = isPortableEntryPointCall ? "*" : ""; w.Write( $$""" {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, InteropSignature.TokenToNameType(returnToken), isPortableEntryPointCall)}}(PCODE {{(isPortableEntryPointCall ? "pPortableEntryPoint" : "pcode")}}, int8_t* pArgs, int8_t* pRet) {{{(isPortableEntryPointCall ? "\n alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK;" : "")}} {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{argTypes}}{{portableEntrypointDeclaration}}) = {{portableEntrypointPointerRD}}({{result.nativeType}} ({{portableEntrypointPointerRD}}*)({{portableEntrypointStackDeclaration}}{{argTypes}}{{portableEntrypointDeclaration}})){{(isPortableEntryPointCall ? "(pPortableEntryPoint)" : "pcode")}}; {{(result.isVoid ? "" : $"*(({result.nativeType}*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{string.Join(", ", ArgsWithSlotOffsets(args))}}{{portableEntrypointParam}}); } """); } catch (InvalidSignatureCharException e) { throw new LogAsErrorException( $"Cannot generate an interop thunk for '{cookies[signature]}': its signature '{signature}' contains {WasmLowering.DescribeSigChar(e.Char)}, " + "which interop thunks cannot pass. Take it by reference, or wrap it in a blittable struct."); } catch (LogAsErrorException e) { // The only place that still knows which method the signature came from. throw new LogAsErrorException($"Cannot generate an interop thunk for '{cookies[signature]}': {e.Message}"); } } w.Write( $$""" } const StringToPortableSigThunk g_portableCallHelperThunks[] = { {{string.Join($",{w.NewLine}", signatures.Select(ThunkEntry))}} }; const size_t g_portableCallHelperThunksCount = sizeof(g_portableCallHelperThunks) / sizeof(g_portableCallHelperThunks[0]); """); static string ThunkEntry(string signature) { List<string> tokens = InteropSignature.ParseSignatureTokens(signature); bool isPortableEntryPointCall = IsPortableEntryPointCall(tokens); if (isPortableEntryPointCall) tokens.RemoveAt(tokens.Count - 1); RemoveAsyncCallMarker(tokens); string name = CallFuncName(Args(tokens), InteropSignature.TokenToNameType(tokens[0]), isPortableEntryPointCall); return $" {{ \"M{signature}\", (void*)&{name} }}"; } static List<string> Args(List<string> tokens) => tokens.Count > 1 ? tokens.GetRange(1, tokens.Count - 1) : []; static List<string> ArgsWithSlotOffsets(List<string> args) { List<string> result = []; int slot = 0; foreach (string token in args) { if (token[0] == 'A') slot = (slot + 1) & ~1; result.Add($"{InteropSignature.TokenToArgType(token)}({slot})"); slot += InteropSignature.TokenToSlotCount(token); } return result; } static (bool IsVoid, string NativeType) Result(string returnToken) { // For struct returns, use the typedef so emcc generates the correct sret ABI if (returnToken[0] == 'S' && returnToken.Length > 1) return (false, $"portable_callhelper_ret_S{InteropSignature.GetStructSize(returnToken)}"); return (returnToken == "v", InteropSignature.TokenToNativeType(returnToken)); } static bool IsPortableEntryPointCall(List<string> tokens) => tokens.Count > 0 && tokens[^1] == "p"; static void RemoveAsyncCallMarker(List<string> tokens) { int asyncMarkerIndex = tokens.IndexOf("a"); if (asyncMarkerIndex >= 0) tokens.RemoveAt(asyncMarkerIndex); } } private static string CallFuncName(List<string> args, string result, bool isPortableEntryPointCall) { string paramTypes = args.Count > 0 ? string.Join("_", args.Select(InteropSignature.TokenToNameType)) : "Void"; return $"CallFunc_{paramTypes}_Ret{result}{(isPortableEntryPointCall ? "_PE" : "")}"; } } }