The duplicate is the Apple crypto native library ending up in the same process twice — once dynamically (the running .NET host) and once statically baked into our AOT dylib.
-
Host already has it (dynamically): the normal dotnet host loads the shared framework, which includes libSystem.Security.Cryptography.Native.Apple.dylib. That dylib contains pal_swiftbindings.o, whose Swift interop registers ObjC classes (HashBox, X25519KeyBox) with the process-wide ObjC runtime at load time.
-
NativeAOT bakes a second copy in (statically): in Microsoft.NETCore.Native.Unix.targets (ILCompiler 11.0.0-preview.5.26261.101), line ~150:
<NetCoreAppNativeLibrary Include="System.Security.Cryptography.Native.Apple"
Condition="'$(_IsApplePlatform)' == 'true'" />
The only condition is "is this an Apple platform" — there is no '$(NativeLib)' != 'Shared' guard. So our NativeLib=Shared output (libdotnet-aot.dylib) statically links the .a version of the same crypto library, including the same pal_swiftbindings.o and its HashBox/X25519KeyBox classes.
-
ObjC class names are process-global → collision: our dylib is loaded into the host process that already registered those classes from the framework copy. The dynamic loader then prints to stderr:
objc[...]: Class HashBox is implemented in both .../libSystem.Security.Cryptography.Native.Apple.dylib and .../libdotnet-aot.dylib. One of the two will be used. Which one is undefined.
That stderr line is what trips the StdErr.Should().BeEmpty() assertions in our tests.
Root flaw: the NativeAOT targets assume the output is a self-contained app that needs crypto statically baked in. For a NativeLib=Shared library loaded into an already-running .NET process, that static copy is redundant and collides with the framework's dynamic copy. The proper upstream fix looks like guarding line ~150 (and the matching DirectPInvoke) with '$(NativeLib)' != 'Shared' so shared-library AOT outputs don't re-embed framework-provided native libs.
Our RemoveAppleCryptoFromNativeAotLink target is essentially doing that guard manually from the csproj after the fact. Does guarding these NetCoreAppNativeLibrary/DirectPInvoke items on NativeLib != Shared sound like the right runtime-side fix, or is there a preferred way to handle native-lib linking for shared AOT outputs that load into an existing runtime?
Originally posted by @NikolaMilosavljevic in dotnet/sdk#54384 (comment)
The duplicate is the Apple crypto native library ending up in the same process twice — once dynamically (the running .NET host) and once statically baked into our AOT dylib.
Host already has it (dynamically): the normal
dotnethost loads the shared framework, which includeslibSystem.Security.Cryptography.Native.Apple.dylib. That dylib containspal_swiftbindings.o, whose Swift interop registers ObjC classes (HashBox,X25519KeyBox) with the process-wide ObjC runtime at load time.NativeAOT bakes a second copy in (statically): in
Microsoft.NETCore.Native.Unix.targets(ILCompiler11.0.0-preview.5.26261.101), line ~150:The only condition is "is this an Apple platform" — there is no
'$(NativeLib)' != 'Shared'guard. So ourNativeLib=Sharedoutput (libdotnet-aot.dylib) statically links the.aversion of the same crypto library, including the samepal_swiftbindings.oand itsHashBox/X25519KeyBoxclasses.ObjC class names are process-global → collision: our dylib is loaded into the host process that already registered those classes from the framework copy. The dynamic loader then prints to stderr:
That stderr line is what trips the
StdErr.Should().BeEmpty()assertions in our tests.Root flaw: the NativeAOT targets assume the output is a self-contained app that needs crypto statically baked in. For a
NativeLib=Sharedlibrary loaded into an already-running .NET process, that static copy is redundant and collides with the framework's dynamic copy. The proper upstream fix looks like guarding line ~150 (and the matchingDirectPInvoke) with'$(NativeLib)' != 'Shared'so shared-library AOT outputs don't re-embed framework-provided native libs.Our
RemoveAppleCryptoFromNativeAotLinktarget is essentially doing that guard manually from the csproj after the fact. Does guarding theseNetCoreAppNativeLibrary/DirectPInvokeitems onNativeLib != Sharedsound like the right runtime-side fix, or is there a preferred way to handle native-lib linking for shared AOT outputs that load into an existing runtime?Originally posted by @NikolaMilosavljevic in dotnet/sdk#54384 (comment)