Skip to content
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

Merged
merged 19 commits into from
Dec 8, 2023

Conversation

yowl
Copy link
Contributor

@yowl yowl commented Nov 29, 2023

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 normal DllImports, 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.

@yowl yowl closed this Nov 29, 2023
@yowl yowl reopened this Nov 29, 2023
@yowl
Copy link
Contributor Author

yowl commented Nov 29, 2023

cc @dotnet/nativeaot-llvm

@yowl yowl marked this pull request as ready for review November 29, 2023 12:34
@yowl yowl changed the title Implement WasmImportlinkage [NativeAOT-LLVM] Implement WasmImportlinkage Nov 29, 2023
Comment on lines 1 to 2
// No copyright header as this is largely a cut and paste from the standard
// Emscripten produced shell

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.

Copy link
Contributor Author

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.

Comment on lines 817 to 824
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);
}
}
Copy link

@SingleAccretion SingleAccretion Nov 29, 2023

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

private readonly Dictionary<string, ExternMethodAccessorNode> _externSymbolsWithAccessors = new();
to have a key of a struct with the necessary fields to do this segregation. Does that sound correct?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Copy link
Contributor Author

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:

  1. The original which just tests we place the import in the correct module
  2. A test for not importing anything if there are duplciate module/func names
  3. A test for the same function in different modules (a minor variation on 1 I suppose)

Do we want some others?

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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.

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>

Copy link
Contributor Author

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

@jkotas jkotas added the area-NativeAOT-LLVM LLVM generation for Native AOT compilation (including Web Assembly) label Dec 3, 2023
ignore module name for direct pinokes
compare abi signatures, not managed signatures
add WasmImportLinkage to PInvokeAttributes
docs/using-nativeaot/compiling.md Outdated Show resolved Hide resolved
Comment on lines 130 to 133
<ItemGroup Condition="'$(NativeCodeGen)' == 'llvm'">
<DirectPInvoke Include="libSystem.Globalization.Native" />
<DirectPInvoke Include="libSystem.Native" />
</ItemGroup>
Copy link

@SingleAccretion SingleAccretion Dec 5, 2023

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 501 to 502
if(HasCustomAttribute("System.Runtime.InteropServices", "WasmImportLinkageAttribute"))
attributes |= PInvokeAttributes.WasmImportLinkage;

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.

Copy link
Contributor Author

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" />

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will do.

Comment on lines 98 to 99
// 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.

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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);

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?

Copy link
Contributor Author

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.

Comment on lines 50 to 54
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);

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:

  1. Create a new WasmConfigurablePInvokePolicy : ConfigurablePInvokePolicy that would do override GenerateDirectCall somewhat like so:
public override bool GenerateDirectCall(MethodDesc method, out string externName)
{
    if (IsWasmImport(method))
    {
        externName = "__wasm_import_" + moduleName + "_" + externName;
        return true;
    }
}
  1. Leave all of the accessor code essentially unchanged.
  2. 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_.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, thanks.

src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs Outdated Show resolved Hide resolved
@@ -15,13 +15,19 @@
<CMakeProjectReference Include="CMakeLists.txt" />
</ItemGroup>

<ItemGroup>
<DirectPInvoke Include="Program!CallMe" />

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 *?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines 1639 to 1649
[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);
}

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 1608 to 1613
[System.Runtime.InteropServices.UnmanagedCallersOnly(EntryPoint = "JustForRooting")]
private static void RootFuncDup(int x)
{
WasmImportFuncDup1(0);
WasmImportFuncDup2();
}

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.

Copy link
Contributor Author

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.

src/tests/nativeaot/SmokeTests/HelloWasm/HelloWasm.cs Outdated Show resolved Hide resolved
CallAbiIncompatFunctionWithInt(0);
CallAbiIncompatFunctionWithFloat(1);
}
#endif

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing tests.

yowl and others added 5 commits December 5, 2023 07:14
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
@yowl
Copy link
Contributor Author

yowl commented Dec 6, 2023

Think the last commit addresses the remaining review comments, thanks.

{
if (IsWasmImport(method, out PInvokeMetadata pInvokeMetadata))
{
externName = "__wasm_import_" + pInvokeMetadata.Module + "_" + pInvokeMetadata.Name;

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)?

Copy link
Contributor Author

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.

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

Copy link
Contributor Author

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())

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.

Copy link
Contributor Author

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;
}

Copy link

@SingleAccretion SingleAccretion left a 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)?

@yowl
Copy link
Contributor Author

yowl commented Dec 7, 2023

Thanks for the extended review!

Copy link
Member

@jkotas jkotas left a 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.

@jkotas jkotas merged commit d6762a9 into dotnet:feature/NativeAOT-LLVM Dec 8, 2023
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-NativeAOT-LLVM LLVM generation for Native AOT compilation (including Web Assembly)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants