Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/coreclr/hosts/corerun/wasm/testkit.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,24 @@ function(corerun_kit_export)
# or, worse, hold per-configuration defaults the repo never actually uses.
# corerun's include directories are likewise not exported: the generated
# sources only need the headers shipped in the kit.
set(compileResponse "")
string(APPEND compileResponse "-O2\n")
string(APPEND compileResponse "-fwasm-exceptions\n")
string(APPEND compileResponse "-sWASM_LEGACY_EXCEPTIONS=0\n")
string(APPEND compileResponse "-msimd128\n")
string(APPEND compileResponse "-DGEN_PINVOKE=1\n")
# The user response is the flags every object linked into the test corerun must
# agree on -- the exception model above all, which wasm-ld rejects if it is
# mixed. It carries no generated-helper-only flags, mirroring the app build's
# split between user NativeFileReference sources and the crossgen2-emitted
# call tables (BrowserWasmApp.CoreCLR.targets: _EmccCFlags vs _EmccCFlagsGenerated).
set(userCompileResponse "")
string(APPEND userCompileResponse "-O2\n")
string(APPEND userCompileResponse "-fwasm-exceptions\n")
string(APPEND userCompileResponse "-sWASM_LEGACY_EXCEPTIONS=0\n")
string(APPEND userCompileResponse "-msimd128\n")
string(APPEND userCompileResponse "-DGEN_PINVOKE=1\n")

# The generated call tables additionally force-include coreclr_compat.h (which
# injects CoreCLR type/macro prereqs like MethodDesc/ULONG/LOG) and add the kit
# header search path. Those are deliberately kept off user sources: a raw .c/.cpp
# NativeFileReference must not inherit those typedefs/macros, which would collide
# with arbitrary user code.
set(compileResponse "${userCompileResponse}")
string(APPEND compileResponse "-I\"include\"\n")
string(APPEND compileResponse "-include\n\"include/coreclr_compat.h\"\n")

Expand All @@ -243,13 +255,16 @@ function(corerun_kit_export)
TARGET corerun_static)
file(GENERATE OUTPUT "${generatedDirectory}/corerun-compile.rsp" CONTENT "${compileResponse}"
TARGET corerun_static)
file(GENERATE OUTPUT "${generatedDirectory}/corerun-compile-user.rsp" CONTENT "${userCompileResponse}"
TARGET corerun_static)

foreach(library IN LISTS kitLibraries)
install(FILES "$<TARGET_FILE:${library}>" DESTINATION ${kitDestination} COMPONENT ${KIT_COMPONENT})
endforeach()
install(FILES ${kitFiles} DESTINATION ${kitDestination} COMPONENT ${KIT_COMPONENT})
install(FILES ${KIT_JS_LIBRARIES} "${KIT_EXTERN_POST_JS}" DESTINATION ${kitDestination} COMPONENT ${KIT_COMPONENT})
install(FILES "${generatedDirectory}/corerun-link.rsp" "${generatedDirectory}/corerun-compile.rsp"
"${generatedDirectory}/corerun-compile-user.rsp"
DESTINATION ${kitDestination} COMPONENT ${KIT_COMPONENT})

# Headers the generated call tables include, plus the compatibility header
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,87 @@ public void PortableCallHelpersGeneratorEncodesTypesTheSameWayInAndOutOfASignatu
Assert.Equal(tokens[1], InteropSignature.GetAbiToken(type));
}

[Theory]
[InlineData("int", "System.Int128", "l2")]
[InlineData("System.Int128", "int", "l2")]
[InlineData("int", "System.Runtime.Intrinsics.Vector128<int>", "V")]
[InlineData("System.Runtime.Intrinsics.Vector128<int>", "int", "V")]
public void PortableCallHelpersGeneratorRejectsUnsupportedSignatureTokens(
string returnType, string parameterType, string expectedToken)
{
string source = $$"""
using System;
using System.Runtime.InteropServices;

public static class Exports
{
[UnmanagedCallersOnly(EntryPoint = "callback")]
public static {{returnType}} Handle({{parameterType}} value) => default;
}
""";

AssertPortableCallHelpersGeneratorRejects(source, $"has unsupported signature token '{expectedToken}'");
}

[Fact]
public void PortableCallHelpersGeneratorRejectsHiddenReturnBufferCallbacks()
{
string source = """
using System.Runtime.InteropServices;

public struct Pair
{
public int First;
public int Second;
}

public static class Exports
{
[UnmanagedCallersOnly(EntryPoint = "callback")]
public static Pair Handle(int value) => default;
}
""";

AssertPortableCallHelpersGeneratorRejects(source, "uses a hidden return buffer");
}

private void AssertPortableCallHelpersGeneratorRejects(string source, string expectedError)
{
string workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(workingDirectory);

try
{
string inputAssembly = CompileCallbackAssembly(source, Path.Combine(workingDirectory, "Callbacks.dll"));
var options = new PortableCallHelpersGeneratorOptions
{
OutputDirectory = Path.Combine(workingDirectory, "generated"),
TargetOS = "browser",
PInvokeModules = new[] { "libSystem.Native" },
};

var log = new StringWriter();
int exitCode = PortableCallHelpersGenerator.Run(
CreateWasmContext(inputAssembly), options, new Logger(log, isVerbose: false));

Assert.Equal(1, exitCode);
Assert.Contains(expectedError, log.ToString());
}
finally
{
// The type system maps an input assembly with FileShare.Read and never releases it - the
// context is not disposable - so on Windows the compiled input cannot be deleted while
// this process lives. Cleaning up is best effort rather than a second way to fail.
try
{
Directory.Delete(workingDirectory, recursive: true);
}
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
{
}
}
}

private const string CoreLibSimpleName = "System.Private.CoreLib";

/// <summary>
Expand Down Expand Up @@ -697,6 +778,9 @@ public static class Exports
{
Assert.Equal(0, exitCode);
Assert.DoesNotContain("declares more than one", log.ToString());
string reverseHelpers = File.ReadAllText(Path.Combine(outputDirectory, "callhelpers-reverse.cpp"));
Assert.Contains("__atomic_load_n", reverseHelpers);
Assert.Contains("__atomic_store_n", reverseHelpers);
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using System.Linq;
using System.Text;

using ILCompiler.DependencyAnalysis.Wasm;
using Internal.JitInterface;
using Internal.TypeSystem;
using Internal.TypeSystem.Ecma;
using Internal.TypeSystem.Interop;
Expand Down Expand Up @@ -246,6 +248,7 @@ public void EmitNativeToInterp(TextWriter w, List<PInvokeCallback> callbacks)
// The current approach has limitations with overloaded methods.
extern "C" void LookupUnmanagedCallersOnlyMethodByName(const char* fullQualifiedTypeName, const char* methodName, MethodDesc** ppMD);
extern "C" void ExecuteInterpretedMethodFromUnmanaged(MethodDesc* pMD, int8_t* args, size_t argSize, int8_t* ret, PCODE callerIp);
extern "C" void* GetR2RNativeCodeForUnmanagedCallersOnly(MethodDesc* pMD);

""");

Expand All @@ -254,6 +257,7 @@ public void EmitNativeToInterp(TextWriter w, List<PInvokeCallback> callbacks)
callbacks.Sort(new PInvokeCallbackComparer());
foreach (PInvokeCallback cb in callbacks)
{
RejectUnsupportedCallbackSignature(cb);
cb.EntrySymbol = FixedSymbolName(cb);

if (!callbackNames.Add(cb.EntrySymbol))
Expand Down Expand Up @@ -281,11 +285,36 @@ public void EmitNativeToInterp(TextWriter w, List<PInvokeCallback> callbacks)
// those instead. Every other type this emits reaches the slot unchanged through a cast.
bool CarriesBits(int i) => parameterCTypes[i] is "float" or "double";
string argsDeclaration = parameterCount > 0
? $"\n int64_t args[{parameterCount}] = {{ {string.Join(", ", Enumerable.Range(0, parameterCount).Select(i => CarriesBits(i) ? "0" : $"(int64_t)arg{i}"))} }};\n"
+ string.Concat(Enumerable.Range(0, parameterCount).Where(CarriesBits).Select(i => $" memcpy(&args[{i}], &arg{i}, sizeof(arg{i}));\n"))
? $"\n\n int64_t args[{parameterCount}] = {{ {string.Join(", ", Enumerable.Range(0, parameterCount).Select(i => CarriesBits(i) ? "0" : $"(int64_t)arg{i}"))} }};"
+ string.Concat(Enumerable.Range(0, parameterCount).Where(CarriesBits).Select(i => $"\n memcpy(&args[{i}], &arg{i}, sizeof(arg{i}));"))
: string.Empty;
string parametersDeclaration = string.Join(", ", parameterCTypes.Select((p, i) => $"{p} arg{i}"));
string arguments = string.Join(", ", Enumerable.Range(0, parameterCount).Select(i => $"arg{i}"));
// A partial R2R image can compile an UnmanagedCallersOnly callback to native code. That R2R code
// is the directly-callable native entrypoint (same ABI as this wrapper's parameters), so dispatch
// to it and skip the interpreter/interp->R2R path entirely.
string r2rVar = $"R2RCode_{cb.EntrySymbol}";
string paramTypesOnly = string.Join(", ", parameterCTypes);
string r2rDispatch = cb.IsVoid
? $"((void(*)({paramTypesOnly}))r2r)({arguments});{w.NewLine} return;"
: $"return (({MapType(cb.ReturnType)}(*)({paramTypesOnly}))r2r)({arguments});";
Comment thread
pavelsavara marked this conversation as resolved.
// Cache the resolved entrypoint in a per-callback static, published with acquire/release
// atomics: these are native entry points that can be entered concurrently, and the value is
// computed identically on every call, so the racing read/write is benign but must not tear.
string r2rStaticDecl = $"{w.NewLine}static void* {r2rVar} = (void*)(intptr_t)-1;";

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.

could the new r2r section stuff not be inline below where it is much more readable?

string r2rSection =
w.NewLine + " // Prefer the R2R native entrypoint when this callback was compiled (partial R2R)."
+ w.NewLine + " // Resolve once and cache; a method's native-code availability is fixed after first prepare."
+ w.NewLine + $" void* r2r = __atomic_load_n(&{r2rVar}, __ATOMIC_ACQUIRE);"
+ w.NewLine + " if (r2r == (void*)(intptr_t)-1)"
+ w.NewLine + " {"
+ w.NewLine + $" r2r = GetR2RNativeCodeForUnmanagedCallersOnly(MD_{cb.EntrySymbol});"
+ w.NewLine + $" __atomic_store_n(&{r2rVar}, r2r, __ATOMIC_RELEASE);"
+ w.NewLine + " }"
+ w.NewLine + " if (r2r != nullptr)"
+ w.NewLine + " {"
+ w.NewLine + $" {r2rDispatch}"
+ w.NewLine + " }";
string exportFunction = cb.IsExport ?
$$"""

Expand All @@ -298,16 +327,16 @@ public void EmitNativeToInterp(TextWriter w, List<PInvokeCallback> callbacks)
w.Write(
$$"""

static MethodDesc* MD_{{cb.EntrySymbol}} = nullptr;
static MethodDesc* MD_{{cb.EntrySymbol}} = nullptr;{{r2rStaticDecl}}
Comment thread
pavelsavara marked this conversation as resolved.
static {{
MapType(cb.ReturnType)}} Call_{{cb.EntrySymbol}}({{parametersDeclaration}})
{{{argsDeclaration}}
{
// Lazy lookup of MethodDesc for the function export scenario.
if (!MD_{{cb.EntrySymbol}})
{
LookupUnmanagedCallersOnlyMethodByName("{{cb.TypeFullName}}, {{cb.AssemblyName}}", "{{cb.MethodName}}", &MD_{{cb.EntrySymbol}});
}{{
(!cb.IsVoid ? $"{w.NewLine}{w.NewLine} {MapType(cb.ReturnType)} result;" : "")}}
}{{r2rSection}}{{argsDeclaration}}{{
(!cb.IsVoid ? $"{w.NewLine} {MapType(cb.ReturnType)} result;" : "")}}
ExecuteInterpretedMethodFromUnmanaged(MD_{{cb.EntrySymbol}}, {{argsArgs}}, {{(cb.IsVoid ? "nullptr" : "(int8_t*)&result")}}, (PCODE)&Call_{{cb.EntrySymbol}});{{
(!cb.IsVoid ? $"{w.NewLine} return result;" : "")}}
}{{exportFunction}}
Expand Down Expand Up @@ -350,6 +379,43 @@ static void RejectAmbiguousExport(PInvokeCallback cb)
throw new LogAsErrorException(
$"Exported callback '{cb.EntryPoint}' cannot be resolved at run time: '{cb.TypeFullName}' declares more than one [UnmanagedCallersOnly] method named '{cb.MethodName}', and the runtime looks them up by name alone. Give them distinct names: {string.Join(", ", ambiguous)}");
}

static void RejectUnsupportedCallbackSignature(PInvokeCallback cb)
{
List<string> loweredTokens = InteropSignature.ParseSignatureTokens(
InteropSignature.GetMethodSignature(cb.Method, WasmLowering.LoweringFlags.IsUnmanagedCallersOnly));
string token = null;
for (int i = 1; i < loweredTokens.Count; i++)
{
if (IsUnsupportedToken(loweredTokens[i]))
{
token = loweredTokens[i];
break;
}
}

if (token is null && !cb.IsVoid)
{
string returnToken = InteropSignature.GetAbiToken(cb.ReturnType);
if (IsUnsupportedToken(returnToken))
token = returnToken;
}

if (token is not null)
{
throw new LogAsErrorException(
$"UnmanagedCallersOnly callback '{cb.Method}' has unsupported signature token '{token}', which the generated native wrapper does not support.");
}

if (!cb.IsVoid && IsPassedByReference(cb.ReturnType))
{
throw new LogAsErrorException(
$"UnmanagedCallersOnly callback '{cb.Method}' has return type '{cb.ReturnType}' that uses a hidden return buffer, which the generated native wrapper does not support.");
Comment thread
pavelsavara marked this conversation as resolved.
}

static bool IsUnsupportedToken(string token) =>
token is "V" || InteropSignature.IsMultiSlotToken(token);
}
}

private string CEntryPoint(PInvokeInfo pinvoke)
Expand Down
Loading
Loading