Support `DllImportSearchPath.AssemblyDirectory` for NativeAOT applications (#90120)
authorAaron Robinson <arobins@microsoft.com>
Tue, 8 Aug 2023 14:38:54 +0000 (07:38 -0700)
committerGitHub <noreply@github.com>
Tue, 8 Aug 2023 14:38:54 +0000 (07:38 -0700)
* NativeAOT - Suppress OS dialog for LoadLibrary failures
on Windows.

* Update NativeAOT to load from application
directory when DllImportSearchPath.AssemblyDirectory
is defined.

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeLibrary.NativeAot.Windows.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeLibrary.NativeAot.cs
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SetThreadErrorMode.cs
src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.cs
src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.csproj
src/tests/Interop/NativeLibrary/API/NativeLibraryTests.cs
src/tests/Interop/NativeLibrary/API/NativeLibraryTests.csproj
src/tests/issues.targets

index 6f5587f..bac99a0 100644 (file)
@@ -13,26 +13,35 @@ namespace System.Runtime.InteropServices
         {
             IntPtr hmod;
 
+            // Disable the OS dialogs when failing to load. This matches CoreCLR.
+            uint prev;
+            bool set = Interop.Kernel32.SetThreadErrorMode(Interop.Kernel32.SEM_FAILCRITICALERRORS | Interop.Kernel32.SEM_NOOPENFILEERRORBOX, out prev);
             if (((uint)flags & 0xFFFFFF00) != 0)
             {
                 hmod = Interop.Kernel32.LoadLibraryEx(libraryName, IntPtr.Zero, (int)((uint)flags & 0xFFFFFF00));
                 if (hmod != IntPtr.Zero)
                 {
-                    return hmod;
+                    goto exit;
                 }
 
-                int lastError = Marshal.GetLastWin32Error();
+                int lastError = Marshal.GetLastPInvokeError();
                 if (lastError != Interop.Errors.ERROR_INVALID_PARAMETER)
                 {
                     errorTracker.TrackErrorCode(lastError);
-                    return hmod;
+                    goto exit;
                 }
             }
 
             hmod = Interop.Kernel32.LoadLibraryEx(libraryName, IntPtr.Zero, flags & 0xFF);
             if (hmod == IntPtr.Zero)
             {
-                errorTracker.TrackErrorCode(Marshal.GetLastWin32Error());
+                errorTracker.TrackErrorCode(Marshal.GetLastPInvokeError());
+            }
+
+        exit:
+            if (set)
+            {
+                Interop.Kernel32.SetThreadErrorMode(prev, out _);
             }
 
             return hmod;
index d79088a..3abe04a 100644 (file)
@@ -97,7 +97,12 @@ namespace System.Runtime.InteropServices
                 else if ((callingAssembly != null) && searchAssemblyDirectory)
                 {
                     // Try to load the module alongside the assembly where the PInvoke was declared.
-                    // This only makes sense in dynamic scenarios (JIT/interpreter), so leaving this out for now.
+                    // For PInvokes where the DllImportSearchPath.AssemblyDirectory is specified, look next to the application.
+                    ret = LoadLibraryHelper(Path.Combine(AppContext.BaseDirectory, currLibNameVariation), loadWithAlteredPathFlags | dllImportSearchPathFlags, ref errorTracker);
+                    if (ret != IntPtr.Zero)
+                    {
+                        return ret;
+                    }
                 }
 
                 ret = LoadLibraryHelper(currLibNameVariation, dllImportSearchPathFlags, ref errorTracker);
index 340b751..c27e924 100644 (file)
@@ -14,6 +14,7 @@ internal static partial class Interop
             uint dwNewMode,
             out uint lpOldMode);
 
-        internal const uint SEM_FAILCRITICALERRORS = 1;
+        internal const int SEM_FAILCRITICALERRORS = 0x00000001;
+        internal const int SEM_NOOPENFILEERRORBOX = 0x00008000;
     }
 }
index d3837ad..644bb79 100644 (file)
@@ -39,6 +39,13 @@ public class DllImportSearchPathsTest
         Assert.Equal(3, sum);
     }
 
+    [ConditionalFact(typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsNativeAot))]
+    public static void AssemblyDirectoryAot_Found()
+    {
+        int sum = NativeLibraryPInvokeAot.Sum(1, 2);
+        Assert.Equal(3, sum);
+    }
+
     [Fact]
     [PlatformSpecific(TestPlatforms.Windows)]
     public static void AssemblyDirectory_Fallback_Found()
@@ -70,3 +77,18 @@ public class NativeLibraryPInvoke
     [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]
     static extern int NativeSum(int arg1, int arg2);
 }
+
+public class NativeLibraryPInvokeAot
+{
+    public static int Sum(int a, int b)
+    {
+        return NativeSum(a, b);
+    }
+
+    // For NativeAOT, validate the case where the native library is next to the AOT application.
+    // The passing of DllImportSearchPath.System32 is done to ensure on Windows the runtime won't fallback
+    // and try to search the application directory by default.
+    [DllImport(NativeLibraryToLoad.Name + "-in-native")]
+    [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.System32)]
+    static extern int NativeSum(int arg1, int arg2);
+}
index e4ca9dc..02284d1 100644 (file)
 
   <Target Name="SetUpSubdirectoryNative" AfterTargets="CopyNativeProjectBinaries">
     <ItemGroup>
-      <_FilesToMove Include="$(OutDir)/libNativeLibrary.*" />
-      <_FilesToMove Include="$(OutDir)/NativeLibrary.*" />
+      <NativeLibrariesToMove Include="$(OutDir)/libNativeLibrary.*" />
+      <NativeLibrariesToMove Include="$(OutDir)/NativeLibrary.*" />
     </ItemGroup>
-    <Move SourceFiles="@(_FilesToMove)" DestinationFiles="@(_FilesToMove -> '$(LibrarySubdirectory)/%(Filename)%(Extension)')"/>
+    <Move SourceFiles="@(NativeLibrariesToMove)" DestinationFiles="@(NativeLibrariesToMove -> '$(LibrarySubdirectory)/%(Filename)%(Extension)')"/>
   </Target>
 
   <Target Name="SetUpSubdirectoryManaged" AfterTargets="Build">
     <ItemGroup>
-      <_FilesToCopy Include="$(OutDir)/$(TargetName).dll" />
+      <AssembliesToCopy Include="$(OutDir)/$(TargetName).dll" />
     </ItemGroup>
-    <Copy SourceFiles="@(_FilesToCopy)" DestinationFiles="@(_FilesToCopy -> '$(LibrarySubdirectory)/%(Filename)%(Extension)')"/>
+    <Copy SourceFiles="@(AssembliesToCopy)" DestinationFiles="@(AssembliesToCopy -> '$(LibrarySubdirectory)/%(Filename)%(Extension)')"/>
+  </Target>
+
+  <Target Name="SetUpAOTDirectory" Condition="'$(TestBuildMode)' == 'nativeaot'" AfterTargets="Build">
+    <ItemGroup>
+      <NativeLibrariesToCopy Include="$(LibrarySubdirectory)/libNativeLibrary.*" />
+      <NativeLibrariesToCopy Include="$(LibrarySubdirectory)/NativeLibrary.*" />
+    </ItemGroup>
+    <Copy SourceFiles="@(NativeLibrariesToCopy)" DestinationFiles="@(NativeLibrariesToCopy -> '$(NativeOutputPath)/%(Filename)-in-native%(Extension)')" />
   </Target>
 </Project>
index 1d29a82..556c006 100644 (file)
@@ -189,6 +189,16 @@ public class NativeLibraryTests : IDisposable
             EXPECT(TryLoadLibrary_WithAssembly(libName, assemblyInSubdirectory, DllImportSearchPath.AssemblyDirectory));
         }
 
+        if (TestLibrary.Utilities.IsNativeAot)
+        {
+            // For NativeAOT, validate the case where the native library is next to the AOT application.
+            // The passing of DllImportSearchPath.System32 is done to ensure on Windows the runtime won't fallback
+            // and try to search the application directory by default.
+            string libNameAot = $"{NativeLibraryToLoad.Name}-in-native";
+            EXPECT(LoadLibrary_WithAssembly(libNameAot, assembly, DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.System32));
+            EXPECT(TryLoadLibrary_WithAssembly(libNameAot, assembly, DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.System32));
+        }
+
         if (OperatingSystem.IsWindows())
         {
             string currentDirectory = Environment.CurrentDirectory;
index 16e89df..fdc11a4 100644 (file)
 
   <Target Name="SetUpSubdirectoryNative" AfterTargets="CopyNativeProjectBinaries">
     <ItemGroup>
-      <AssembliesToCopy Include="$(OutDir)/libNativeLibrary.*" />
-      <AssembliesToCopy Include="$(OutDir)/NativeLibrary.*" />
+      <NativeLibrariesToCopy Include="$(OutDir)/libNativeLibrary.*" />
+      <NativeLibrariesToCopy Include="$(OutDir)/NativeLibrary.*" />
     </ItemGroup>
-    <Copy SourceFiles="@(AssembliesToCopy)" DestinationFiles="@(AssembliesToCopy -> '$(LibrarySubdirectory)/%(Filename)$(FileNameSuffix)%(Extension)')" />
+    <Copy SourceFiles="@(NativeLibrariesToCopy)" DestinationFiles="@(NativeLibrariesToCopy -> '$(LibrarySubdirectory)/%(Filename)$(FileNameSuffix)%(Extension)')" />
   </Target>
 
   <Target Name="SetUpSubdirectoryManaged" AfterTargets="Build">
     </ItemGroup>
     <Copy SourceFiles="@(AssembliesToCopy)" DestinationFiles="@(AssembliesToCopy -> '$(LibrarySubdirectory)/%(Filename)$(FileNameSuffix)%(Extension)')" />
   </Target>
+
+  <Target Name="SetUpAOTDirectory" Condition="'$(TestBuildMode)' == 'nativeaot'" AfterTargets="Build">
+    <ItemGroup>
+      <NativeLibrariesToCopyAOT Include="$(OutDir)/libNativeLibrary.*" />
+      <NativeLibrariesToCopyAOT Include="$(OutDir)/NativeLibrary.*" />
+    </ItemGroup>
+    <Copy SourceFiles="@(NativeLibrariesToCopyAOT)" DestinationFiles="@(NativeLibrariesToCopyAOT -> '$(NativeOutputPath)/%(Filename)-in-native%(Extension)')" />
+  </Target>
 </Project>
index 3dd51b5..a7a23cd 100644 (file)
         <ExcludeList Include="$(XunitTestBinBase)/Interop/MarshalAPI/FunctionPointer/FunctionPtrTest/*">
             <Issue>https://github.com/dotnet/runtimelab/issues/164</Issue>
         </ExcludeList>
-        <ExcludeList Include="$(XunitTestBinBase)/Interop/NativeLibrary/API/NativeLibraryTests/*">
-            <Issue>https://github.com/dotnet/runtimelab/issues/165</Issue>
-        </ExcludeList>
         <ExcludeList Include="$(XunitTestBinBase)/Interop/NativeLibrary/AssemblyLoadContext/ResolveUnmanagedDllTests/*">
             <Issue>https://github.com/dotnet/runtimelab/issues/165</Issue>
         </ExcludeList>