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" : "")}";
        }
    }
}