-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Fix C# native library loading in .NET 5 single-file apps #24744
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
Merged
jtattermusch
merged 4 commits into
grpc:master
from
jtattermusch:net5_singlefile_publish
Nov 30, 2020
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
53390d9
add C# distribtest coverage for .NET5 single file publish
jtattermusch 1f9f3c7
fix native extension loading in .NET5 single-file deployments
jtattermusch 7324c5c
Load native extension on .NET core via differentiated DllImports
jtattermusch 840590e
simplify .NET framework loading logic
jtattermusch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,28 +80,25 @@ public NativeMethods NativeMethods | |
| /// <summary> | ||
| /// Detects which configuration of native extension to load and load it. | ||
| /// </summary> | ||
| private static UnmanagedLibrary LoadUnmanagedLibrary() | ||
| private static NativeMethods LoadNativeMethodsLegacyNetFramework() | ||
| { | ||
| // TODO: allow customizing path to native extension (possibly through exposing a GrpcEnvironment property). | ||
| // See https://github.com/grpc/grpc/pull/7303 for one option. | ||
| var assemblyDirectory = Path.GetDirectoryName(GetAssemblyPath()); | ||
| var assemblyDirectory = GetAssemblyDirectory(); | ||
|
|
||
| // With "classic" VS projects, the native libraries get copied using a .targets rule to the build output folder | ||
| // alongside the compiled assembly. | ||
| // With dotnet cli projects targeting net45 framework, the native libraries (just the required ones) | ||
| // are similarly copied to the built output folder, through the magic of Microsoft.NETCore.Platforms. | ||
| var classicPath = Path.Combine(assemblyDirectory, GetNativeLibraryFilename()); | ||
|
|
||
| // With dotnet cli project targeting netcoreappX.Y, projects will use Grpc.Core assembly directly in the location where it got restored | ||
| // by nuget. We locate the native libraries based on known structure of Grpc.Core nuget package. | ||
| // When "dotnet publish" is used, the runtimes directory is copied next to the published assemblies. | ||
| string runtimesDirectory = string.Format("runtimes/{0}/native", GetRuntimeIdString()); | ||
| var netCorePublishedAppStylePath = Path.Combine(assemblyDirectory, runtimesDirectory, GetNativeLibraryFilename()); | ||
| var netCoreAppStylePath = Path.Combine(assemblyDirectory, "../..", runtimesDirectory, GetNativeLibraryFilename()); | ||
|
|
||
| // Look for the native library in all possible locations in given order. | ||
| string[] paths = new[] { classicPath, netCorePublishedAppStylePath, netCoreAppStylePath}; | ||
| return new UnmanagedLibrary(paths); | ||
| string[] paths = new[] { classicPath }; | ||
|
|
||
| // TODO(jtattermusch): the UnmanagedLibrary mechanism for loading the native extension while avoiding | ||
| // direct use of DllImport is quite complicated and is currently only needed to cover some niche scenarios | ||
| // (such legacy .NET Framework projects that use assembly shadowing) - everything else can be covered | ||
| // by using the [DllImport]. We should investigate the possibility of eliminating UnmanagedLibrary completely | ||
| // in the future. | ||
| return new NativeMethods(new UnmanagedLibrary(paths)); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -117,7 +114,43 @@ private static NativeMethods LoadNativeMethods() | |
| { | ||
| return LoadNativeMethodsXamarin(); | ||
| } | ||
| return new NativeMethods(LoadUnmanagedLibrary()); | ||
| if (PlatformApis.IsNetCore) | ||
| { | ||
| // On .NET Core, native libraries are a supported feature and the SDK makes | ||
| // sure that the native library is made available in the right location and that | ||
| // they will be discoverable by the [DllImport] default loading mechanism, | ||
| // even in some of the more exotic situations such as single file apps. | ||
| // | ||
| // While in theory, we could just [DllImport("grpc_csharp_ext")] for all the platforms | ||
| // and operating systems, the native libraries in the nuget package | ||
| // need to be laid out in a way that still allows things to work well under | ||
| // the legacy .NET Framework (where native libraries are a concept unknown to the runtime). | ||
| // Therefore, we use several flavors of the DllImport attribute | ||
| // (e.g. the ".x86" vs ".x64" suffix) and we choose the one we want at runtime. | ||
| // The classes with the list of DllImport'd methods are code generated, | ||
| // so having more than just one doesn't really bother us. | ||
|
|
||
| // on Windows, the DllImport("grpc_csharp_ext.x64") doesn't work for some reason, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was bug dotnet/coreclr#17505 . It is fixed in .NET Core 3.1, but I assume you still want this to work on .NET Core 2.1. |
||
| // but DllImport("grpc_csharp_ext.x64.dll") does, so we need a special case for that. | ||
| bool useDllSuffix = PlatformApis.IsWindows; | ||
| if (PlatformApis.Is64Bit) | ||
| { | ||
| if (useDllSuffix) | ||
| { | ||
| return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x64_dll()); | ||
| } | ||
| return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x64()); | ||
| } | ||
| else | ||
| { | ||
| if (useDllSuffix) | ||
| { | ||
| return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x86_dll()); | ||
| } | ||
| return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x86()); | ||
| } | ||
| } | ||
| return LoadNativeMethodsLegacyNetFramework(); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -139,6 +172,10 @@ private static NativeMethods LoadNativeMethodsUnity() | |
|
|
||
| /// <summary> | ||
| /// Return native method delegates when running on the Xamarin platform. | ||
| /// On Xamarin, the standard <c>[DllImport]</c> loading logic just works | ||
| /// as the native library metadata is provided by the <c>AndroidNativeLibrary</c> or | ||
| /// <c>NativeReference</c> items in the Xamarin projects (injected automatically | ||
| /// by the Grpc.Core.Xamarin nuget). | ||
| /// WARNING: Xamarin support is experimental and work-in-progress. Don't expect it to work. | ||
| /// </summary> | ||
| private static NativeMethods LoadNativeMethodsXamarin() | ||
|
|
@@ -147,17 +184,23 @@ private static NativeMethods LoadNativeMethodsXamarin() | |
| { | ||
| return new NativeMethods(new NativeMethods.DllImportsFromSharedLib()); | ||
| } | ||
| // not tested yet | ||
| return new NativeMethods(new NativeMethods.DllImportsFromStaticLib()); | ||
| } | ||
|
|
||
| private static string GetAssemblyPath() | ||
| private static string GetAssemblyDirectory() | ||
| { | ||
| var assembly = typeof(NativeExtension).GetTypeInfo().Assembly; | ||
| #if NETSTANDARD | ||
| // Assembly.EscapedCodeBase does not exist under CoreCLR, but assemblies imported from a nuget package | ||
| // don't seem to be shadowed by DNX-based projects at all. | ||
| return assembly.Location; | ||
| var assemblyLocation = assembly.Location; | ||
| if (!string.IsNullOrEmpty(assemblyLocation)) | ||
| { | ||
| return Path.GetDirectoryName(assemblyLocation); | ||
| } | ||
| // In .NET5 single-file deployments, assembly.Location won't be available | ||
| // Also see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#other-considerations | ||
| return AppContext.BaseDirectory; | ||
| #else | ||
| // If assembly is shadowed (e.g. in a webapp), EscapedCodeBase is pointing | ||
| // to the original location of the assembly, and Location is pointing | ||
|
|
@@ -167,9 +210,9 @@ private static string GetAssemblyPath() | |
| var escapedCodeBase = assembly.EscapedCodeBase; | ||
| if (IsFileUri(escapedCodeBase)) | ||
| { | ||
| return new Uri(escapedCodeBase).LocalPath; | ||
| return Path.GetDirectoryName(new Uri(escapedCodeBase).LocalPath); | ||
| } | ||
| return assembly.Location; | ||
| return Path.GetDirectoryName(assembly.Location); | ||
| #endif | ||
| } | ||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 changes in this method should not be needed with your new fix.