-
Notifications
You must be signed in to change notification settings - Fork 206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NativeAOT-LLVM] Implement WasmImportlinkage #2444
Conversation
cc @dotnet/nativeaot-llvm |
// No copyright header as this is largely a cut and paste from the standard | ||
// Emscripten produced shell |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is all of the JS needed? It would be nice to trim it down to a more readable state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have removed most of it.
foreach (MethodDesc methodDesc in node.EnumerateMethods()) | ||
{ | ||
externFunc.AddFunctionAttribute("wasm-import-name", externFuncName); | ||
externFunc.AddFunctionAttribute("wasm-import-module", wasmModuleName); | ||
if (methodDesc.HasCustomAttribute("System.Runtime.InteropServices", "WasmImportLinkageAttribute")) | ||
{ | ||
externFunc.AddFunctionAttribute("wasm-import-name", externFuncName); | ||
externFunc.AddFunctionAttribute("wasm-import-module", methodDesc.GetPInvokeMethodMetadata().Module); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think more is needed to implement the behavior in #2414 (comment).
WasmImportLinkage
-attributed methods create what is effectively a second namespace to the static symbols, so, for example:
[DllImport("xyz", EntryPoint = "xyz_func"), WasmImportLinkage]
void func_1();
[DllImport("*", EntryPoint = "xyz_func")]
void func_2();
Here, we will link in func_2
statically, and func_1
as an xyz_func
import from the xyz
module. It means signature mismatch and AddFunctionAttribute("wasm-import-name")
must be segregated for these two categories too. For example:
[DllImport("xyz", EntryPoint = "xyz_func"), WasmImportLinkage]
void func_2(int arg);
[DllImport("xyz", EntryPoint = "xyz_func"), WasmImportLinkage]
void func_3();
Should report a warning (and choose which signature to import? Or import none?).
While this:
[DllImport("not_xyz", EntryPoint = "xyz_func"), WasmImportLinkage]
void func_2(int arg);
[DllImport("xyz", EntryPoint = "xyz_func"), WasmImportLinkage]
void func_3();
Should not report a warning, etc.
Naturally, a number of tests is needed to verify all these interactions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or import none?
I think that would be safer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
must be segregated
I was thinking that I would change
Line 16 in 7c00d39
private readonly Dictionary<string, ExternMethodAccessorNode> _externSymbolsWithAccessors = new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naturally, a number of tests is needed to verify all these interactions.
There are just 3 tests:
- The original which just tests we place the import in the correct module
- A test for not importing anything if there are duplciate module/func names
- A test for the same function in different modules (a minor variation on 1 I suppose)
Do we want some others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I'd completely forgotten about the changes we were going to do for DirectPInvoke, sorry. I've added a few more tests but I don't think I've got the tests for PNSE, or signature warnings quite right. We suppress IL3049
for HelloWasm and I didn't really understand what is happening with the switch
in TestNativeCallsWithMismatchedSignatures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We suppress IL3049 for HelloWasm and I didn't really understand what is happening with the switch in
TestNativeCallsWithMismatchedSignatures
The basic idea of the test is that invalid PIs do not fail the compilation and do not fail the binding of valid PIs (the latter will not be true for WASM imports, but, oh well). So it's not really testing the warning directly in any special way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- It is fine to defer the work of actually throwing the exception from this PR.
I'm missing something here. If we don't do the generation of the PNSE here how do we do the test for 3:
[DllImport("not_xyz", EntryPoint="func")]
void func_1(); // Throws PNSE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just #if 0
it out for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, a few are like this that throw unmanaged exceptions in the wasm runtime.
@@ -119,18 +119,13 @@ Note that assemblies other than the one being published (e. g. those from refere | |||
``` | |||
|
|||
#### WebAssembly module imports | |||
Functions in other WebAssembly modules can be imported and invoked using `DllImport` e.g. | |||
Functions in other WebAssembly modules can be imported and invoked using `DllImport` and `WasmImportLinkage` e.g. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WasmImportLinkage
Probably worth mentioning that:
`WasmImportLinkage` is only available when you build against the latest nightly SDK, although you can define it manually as well:
<copy paste minimal definition of WasmImportLinkage>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated the docs with this, thanks
…to detect duplicate wasm imports
Add test for same function name in different module imports
…e custom attribute from the "owner" method.
ignore module name for direct pinokes compare abi signatures, not managed signatures add WasmImportLinkage to PInvokeAttributes
<ItemGroup Condition="'$(NativeCodeGen)' == 'llvm'"> | ||
<DirectPInvoke Include="libSystem.Globalization.Native" /> | ||
<DirectPInvoke Include="libSystem.Native" /> | ||
</ItemGroup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's time for us to start using Microsoft.NETCore.Native.Wasm.props
(which should be renamed to .targets
), and add this there, similar to how it is for Unix.targets
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
if(HasCustomAttribute("System.Runtime.InteropServices", "WasmImportLinkageAttribute")) | ||
attributes |= PInvokeAttributes.WasmImportLinkage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EcmaMethod.cs
is core type system code, representing only ECMA-defined concepts. The attribute checking will have be done elsewhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted
@@ -449,7 +448,6 @@ | |||
<Compile Include="Compiler\DependencyAnalysis\FieldMetadataNode.cs" /> | |||
<Compile Include="Compiler\DependencyAnalysis\ILScanNodeFactory.cs" /> | |||
<Compile Include="Compiler\DictionaryLayoutProvider.cs" /> | |||
<Compile Include="Compiler\DirectPInvokePolicy.cs" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just checked, DirectPInvokePolicy.cs
is actually (unused) upstream code. So the conflict-free strategy here would be to not touch it in this change, merge it, and then delete it upstream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, will do.
src/coreclr/tools/aot/ILCompiler.LLVM/CodeGen/LLVMObjectWriter.cs
Outdated
Show resolved
Hide resolved
// Wasm imports are different if any of module name, function name, ABI signature are different. | ||
// Direct externs are different if the extern function name is different. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasm imports are different if any of module name, function name, ABI signature are different
The WASM file format does support "overloaded" imports, but we do not (since LLVM does not, I believe). So the signature is not part of the symbol resolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this was related to getting the warning out in LLVMObjectWriter, I'll redo that in line with the other comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed now.
Debug.Assert(externFuncModule.GetNamedFunction(externFuncName).Handle == IntPtr.Zero); | ||
LLVMValueRef externFunc = externFuncModule.AddFunction(externFuncName, externFuncType); | ||
WasmImportKey wasmImportKey = new WasmImportKey(node.ExternSymbolKey); | ||
bool externExists = _wasmImportLinkages.TryGetValue(wasmImportKey, out LLVMValueRef externFunc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This I am struggling to understand the need for _wasmImportLinkages
. The node factory should unique accessor nodes for the same WASM import, so why would we be adding the same one twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, it can all be done the same as direct P/Invokes.
internal ExternMethodAccessorNode ExternSymbolWithAccessor(string name, MethodDesc method, ReadOnlySpan<TargetAbiType> sig) | ||
{ | ||
Dictionary<string, ExternMethodAccessorNode> map = _externSymbolsWithAccessors; | ||
Dictionary<ExternSymbolKey, ExternMethodAccessorNode> map = _externSymbolsWithAccessors; | ||
PInvokeMetadata pInvokeMethodMetadata = method.GetPInvokeMethodMetadata(); | ||
ExternSymbolKey key = new(pInvokeMethodMetadata.Module, name, pInvokeMethodMetadata.Flags.WasmImportLinkage, sig); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The layering here no longer quite makes sense. Originally, this only served static symbols, so we would go like so:
codegen asks LLVMCodegenCompilation
for an accessor to a static PInvoke (via 'GetExternalMethodAccessor')
LLVMCodegenCompilation asks ConfigurablePInvokePolicy
for a direct symbol name for that static PI
LLVMCodegenCompilation asks LLVMCodegenNodeFactory
to produce a node representing the accessor, for that symbol name
Somewhat non-obviously, but importantly, method/sig
here are auxiliary information essentially only used for diagnostic purposes. Now we are using method
to essentially "skip over" the PI policy, which controls symbol resolution.
I think we can use the PI policy mechanism to our advantage and make this a lot cleaner:
- Create a new
WasmConfigurablePInvokePolicy : ConfigurablePInvokePolicy
that would do overrideGenerateDirectCall
somewhat like so:
public override bool GenerateDirectCall(MethodDesc method, out string externName)
{
if (IsWasmImport(method))
{
externName = "__wasm_import_" + moduleName + "_" + externName;
return true;
}
}
- Leave all of the accessor code essentially unchanged.
- In LLVM object writer, ask the policy whether the accessor node represents a WASM import. Could be simple as asking whether the symbol name starts with
__wasm_import_
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, thanks.
@@ -15,13 +15,19 @@ | |||
<CMakeProjectReference Include="CMakeLists.txt" /> | |||
</ItemGroup> | |||
|
|||
<ItemGroup> | |||
<DirectPInvoke Include="Program!CallMe" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is surprising that this is needed. Isn't it already imported via *
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
[DllImport("StaticModule1", EntryPoint = "CommonStaticFunctionName")] | ||
private static extern int CallAbiCompatFunctionWithInt(int arg); | ||
|
||
[DllImport("StaticModule2", EntryPoint = "CommonStaticFunctionName")] | ||
private static extern uint CallAbiCompatFunctionWitUint(uint arg); | ||
|
||
private static void TestStaticAbiCompatibleSignatures() | ||
{ | ||
StartTest("Static imports with ABI compatible signatures"); | ||
EndTest(CallAbiCompatFunctionWithInt(456) == 456 && CallAbiCompatFunctionWitUint(789) == 789); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add the same kind of test to the WasmImport
section above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
[System.Runtime.InteropServices.UnmanagedCallersOnly(EntryPoint = "JustForRooting")] | ||
private static void RootFuncDup(int x) | ||
{ | ||
WasmImportFuncDup1(0); | ||
WasmImportFuncDup2(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should technically be a try { } catch (EntryPointNotFoundException) { }
test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have implemented like this, but is #if
out for now as we dont throw EntryPointNotFoundException
yet, same situation as PNSE I suppose.
CallAbiIncompatFunctionWithInt(0); | ||
CallAbiIncompatFunctionWithFloat(1); | ||
} | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add the scenario 3
test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the missing tests.
Thanks Co-authored-by: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com>
Introduce WasmConfigurablePInvokePolicy simplifying lots add a few more tests rename Microsoft.NETCore.Native.Wasm.props
…into wasmimportlinkage
add tests
Think the last commit addresses the remaining review comments, thanks. |
{ | ||
if (IsWasmImport(method, out PInvokeMetadata pInvokeMetadata)) | ||
{ | ||
externName = "__wasm_import_" + pInvokeMetadata.Module + "_" + pInvokeMetadata.Name; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pInvokeMetadata.Name
Does this work correctly with PIs that don't specify EntryPoint
(if not, it also needs a test)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to yes, added the test:
private static void TestNamedModuleCallWithoutEntryPoint()
{
StartTest("Wasm import from named module test");
EndTest(ModuleFuncWithoutEntryPoint(77) == 77);
}
[DllImport("ModuleName"), WasmImportLinkage]
private static extern int ModuleFuncWithoutEntryPoint(int x);
Generates
declare i32 @__wasm_import_ModuleName_ModuleFuncWithoutEntryPoint(i32) #4
define ptr @get.__wasm_import_ModuleName_ModuleFuncWithoutEntryPoint() {
GetExternFunc:
ret ptr @__wasm_import_ModuleName_ModuleFuncWithoutEntryPoint
}
attributes #4 = { "wasm-import-module"="ModuleName" "wasm-import-name"="ModuleFuncWithoutEntryPoint" }
@@ -814,10 +814,17 @@ private void GetCodeForExternMethodAccessor(ExternMethodAccessorNode node) | |||
LLVMValueRef externFunc = externFuncModule.AddFunction(externFuncName, externFuncType); | |||
|
|||
// Add import attributes if specified. | |||
if (_compilation.ConfigurableWasmImportPolicy.TryGetWasmModule(externFuncName, out string wasmModuleName)) | |||
// Copy the prefix from WasmConfigurablePInvokePolicy here to avoid making the policy classes public. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to consolidate this logic in the policy.
You can add a virtual bool GenerateWasmImportCall(MethodDesc method, out string name, out string moduleName)
to PInvokeILEmitterConfiguration
, and expose it through PInvokeILProvider
as, e. g., GetWasmImportCallInfo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, yes that's cleaner.
{ | ||
externFunc.AddFunctionAttribute("wasm-import-name", externFuncName); | ||
externFunc.AddFunctionAttribute("wasm-import-module", wasmModuleName); | ||
foreach (MethodDesc method in node.EnumerateMethods()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perf nit: adding a GetSingleMethod()
to the accessor node, like:
_methods switch { MethodDesc methods => method, _ => /* foreach loop over HashSet<MethodDesc> */ }
will avoid allocating the enumerator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I did the switch
more like the one earlier in the file as I couldn't see how to do the foreach with the other style as I think it needs an expression.
switch (_methods)
{
case MethodDesc oneMethod:
return oneMethod;
default:
foreach (MethodDesc method in (HashSet<MethodDesc>)_methods)
{
return method;
}
return null;
}
addresses feedback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for making the changes, this LGTM!
Could you open an issue detailing what is still missing (PNSE/EPNF)?
Thanks for the extended review! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thank you for adding a lot of tests.
This PR implements the new
WasmImportlinkageAttribute
and remove the old policy based mechanism. Added a test for the browser target as it is easier to fulfill the import in Javascript.Added a key
ExternSymbolKey
to_externSymbolsWithAccessors
to distinguish between Wasm imports and normalDllImport
s, Wasm imports of the same function from different modules, and imports of the same name but with different signatures.When writing the object files, added a new dictionary
_wasmImportLinkages
to track emitted functions with the wasm import attributes. This allows us to remove duplicate imports as they behaviour for that is not defined.