Merge pull request #24853 from jtattermusch/backport_24744

Fix C# native library loading in .NET 5 single-file apps (backport to v1.34.x)
pull/24857/head
Jan Tattermusch 4 years ago committed by GitHub
commit 560a898288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 81
      src/csharp/Grpc.Core/Internal/NativeExtension.cs
  2. 1714
      src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs
  3. 82
      templates/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs.template
  4. 5
      test/distrib/csharp/run_distrib_test_dotnetcli.sh

@ -80,28 +80,25 @@ namespace Grpc.Core.Internal
/// <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 @@ namespace Grpc.Core.Internal
{
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,
// 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 @@ namespace Grpc.Core.Internal
/// <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 @@ namespace Grpc.Core.Internal
{
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 @@ namespace Grpc.Core.Internal
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
}

File diff suppressed because it is too large Load Diff

@ -63,6 +63,34 @@
% endfor
}
public NativeMethods(DllImportsFromSharedLib_x86 unusedInstance)
{
% for method in get_native_methods():
this.${method['name']} = DllImportsFromSharedLib_x86.${method['name']};
% endfor
}
public NativeMethods(DllImportsFromSharedLib_x64 unusedInstance)
{
% for method in get_native_methods():
this.${method['name']} = DllImportsFromSharedLib_x64.${method['name']};
% endfor
}
public NativeMethods(DllImportsFromSharedLib_x86_dll unusedInstance)
{
% for method in get_native_methods():
this.${method['name']} = DllImportsFromSharedLib_x86_dll.${method['name']};
% endfor
}
public NativeMethods(DllImportsFromSharedLib_x64_dll unusedInstance)
{
% for method in get_native_methods():
this.${method['name']} = DllImportsFromSharedLib_x64_dll.${method['name']};
% endfor
}
/// <summary>
/// Delegate types for all published native methods. Declared under inner class to prevent scope pollution.
/// </summary>
@ -87,7 +115,7 @@
}
/// <summary>
/// grpc_csharp_ext used a shared library (e.g on Unity Standalone and Android).
/// grpc_csharp_ext used as a shared library (e.g on Unity Standalone and Android).
/// </summary>
internal class DllImportsFromSharedLib
{
@ -98,5 +126,57 @@
public static extern ${method['returntype']} ${method['name']}(${method['params']});
% endfor
}
/// <summary>
/// grpc_csharp_ext used as a shared library (with x86 suffix)
/// </summary>
internal class DllImportsFromSharedLib_x86
{
private const string ImportName = "grpc_csharp_ext.x86";
% for method in get_native_methods():
[DllImport(ImportName)]
public static extern ${method['returntype']} ${method['name']}(${method['params']});
% endfor
}
/// <summary>
/// grpc_csharp_ext used as a shared library (with x64 suffix)
/// </summary>
internal class DllImportsFromSharedLib_x64
{
private const string ImportName = "grpc_csharp_ext.x64";
% for method in get_native_methods():
[DllImport(ImportName)]
public static extern ${method['returntype']} ${method['name']}(${method['params']});
% endfor
}
/// <summary>
/// grpc_csharp_ext used as a shared library (with x86.dll suffix)
/// </summary>
internal class DllImportsFromSharedLib_x86_dll
{
private const string ImportName = "grpc_csharp_ext.x86.dll";
% for method in get_native_methods():
[DllImport(ImportName)]
public static extern ${method['returntype']} ${method['name']}(${method['params']});
% endfor
}
/// <summary>
/// grpc_csharp_ext used as a shared library (with x64.dll suffix)
/// </summary>
internal class DllImportsFromSharedLib_x64_dll
{
private const string ImportName = "grpc_csharp_ext.x64.dll";
% for method in get_native_methods():
[DllImport(ImportName)]
public static extern ${method['returntype']} ${method['name']}(${method['params']});
% endfor
}
}
}

@ -68,9 +68,14 @@ if [ "${SKIP_NET50_DISTRIBTEST}" != "1" ]
then
dotnet publish -f net5.0 DistribTestDotNet.csproj
dotnet publish -r linux-x64 -f net5.0 DistribTestDotNet.csproj -p:PublishSingleFile=true --self-contained true --output net5_singlefile_publish
# .NET Core target after dotnet build
dotnet exec bin/Debug/net5.0/DistribTestDotNet.dll
# .NET Core target after dotnet publish
dotnet exec bin/Debug/net5.0/publish/DistribTestDotNet.dll
# binary generated by the single file publish
./net5_singlefile_publish/DistribTestDotNet
fi

Loading…
Cancel
Save