Imported Upstream version 1.36.0
[platform/upstream/grpc.git] / src / csharp / Grpc.Core / Internal / NativeExtension.cs
index 7528ffa..0aa5bb0 100644 (file)
@@ -29,6 +29,8 @@ namespace Grpc.Core.Internal
     /// </summary>
     internal sealed class NativeExtension
     {
+        // Enviroment variable can be used to force loading the native extension from given location.
+        private const string CsharpExtOverrideLocationEnvVarName = "GRPC_CSHARP_EXT_OVERRIDE_LOCATION";
         static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<NativeExtension>();
         static readonly object staticLock = new object();
         static volatile NativeExtension instance;
@@ -78,30 +80,81 @@ namespace Grpc.Core.Internal
         }
 
         /// <summary>
-        /// Detects which configuration of native extension to load and load it.
+        /// Detects which configuration of native extension to load and explicitly loads the dynamic library.
+        /// The explicit load makes sure that we can detect any loading problems early on.
         /// </summary>
-        private static NativeMethods LoadNativeMethodsLegacyNetFramework()
+        private static NativeMethods LoadNativeMethodsUsingExplicitLoad()
         {
-            // TODO: allow customizing path to native extension (possibly through exposing a GrpcEnvironment property).
-            // See https://github.com/grpc/grpc/pull/7303 for one option.
+            // NOTE: a side effect of searching the native extension's library file relatively to the assembly location is that when Grpc.Core assembly
+            // is loaded via reflection from a different app's context, the native extension is still loaded correctly
+            // (while if we used [DllImport], the native extension won't be on the other app's search path for shared libraries).
             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 SDK 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 SDK 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 };
+            string[] paths = new[] { classicPath, netCorePublishedAppStylePath, netCoreAppStylePath};
 
-            // 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.
+            // The UnmanagedLibrary mechanism for loading the native extension while avoiding
+            // direct use of DllImport is quite complicated but it is currently needed to ensure:
+            // 1.) the native extension is loaded eagerly (needed to avoid startup issues)
+            // 2.) less common scenarios (such as loading Grpc.Core.dll by reflection) still work
+            // 3.) loading native extension from an arbitrary location when set by an enviroment variable
+            // TODO(jtattermusch): revisit the possibility of eliminating UnmanagedLibrary completely in the future.
             return new NativeMethods(new UnmanagedLibrary(paths));
         }
 
         /// <summary>
+        /// Loads native methods using the <c>[DllImport(LIBRARY_NAME)]</c> attributes.
+        /// Note that this way of loading the native extension is "lazy" and doesn't
+        /// detect any "missing library" problems until we actually try to invoke the native methods
+        /// (which could be too late and could cause weird hangs at startup)
+        /// </summary>
+        private static NativeMethods LoadNativeMethodsUsingDllImports()
+        {
+            // While in theory, we could just use [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
+            // but DllImport("grpc_csharp_ext.x64.dll") does, so we need a special case for that.
+            // See https://github.com/dotnet/coreclr/pull/17505 (fixed in .NET Core 3.1+)
+            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());
+            }
+        }
+
+        /// <summary>
         /// Loads native extension and return native methods delegates.
         /// </summary>
         private static NativeMethods LoadNativeMethods()
@@ -114,43 +167,27 @@ namespace Grpc.Core.Internal
             {
                 return LoadNativeMethodsXamarin();
             }
-            if (PlatformApis.IsNetCore)
+
+            // Override location of grpc_csharp_ext native library with an environment variable
+            // Use at your own risk! By doing this you take all the responsibility that the dynamic library
+            // is of the correct version (needs to match the Grpc.Core assembly exactly) and of the correct platform/architecture.
+            var nativeExtPathFromEnv = System.Environment.GetEnvironmentVariable(CsharpExtOverrideLocationEnvVarName);
+            if (!string.IsNullOrEmpty(nativeExtPathFromEnv))
             {
-                // 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.
+                return new NativeMethods(new UnmanagedLibrary(new string[] { nativeExtPathFromEnv }));
+            }
 
-                // 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());
-                }
+            if (IsNet5SingleFileApp())
+            {
+                // Ideally we'd want to always load the native extension explicitly
+                // (to detect any potential problems early on and to avoid hard-to-debug startup issues)
+                // but the mechanism we normally use doesn't work when running
+                // as a single file app (see https://github.com/grpc/grpc/pull/24744).
+                // Therefore in this case we simply rely
+                // on the automatic [DllImport] loading logic to do the right thing.
+                return LoadNativeMethodsUsingDllImports();
             }
-            return LoadNativeMethodsLegacyNetFramework();
+            return LoadNativeMethodsUsingExplicitLoad();
         }
 
         /// <summary>
@@ -194,13 +231,14 @@ namespace Grpc.Core.Internal
             // 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.
             var assemblyLocation = assembly.Location;
-            if (!string.IsNullOrEmpty(assemblyLocation))
+            if (string.IsNullOrEmpty(assemblyLocation))
             {
-                return Path.GetDirectoryName(assemblyLocation);
+                // In .NET5 single-file deployments, assembly.Location won't be available
+                // and we can use it for detecting whether we are running as a single file app.
+                // Also see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#other-considerations
+                return null;
             }
-            // 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;
+            return Path.GetDirectoryName(assemblyLocation);
 #else
             // If assembly is shadowed (e.g. in a webapp), EscapedCodeBase is pointing
             // to the original location of the assembly, and Location is pointing
@@ -216,6 +254,12 @@ namespace Grpc.Core.Internal
 #endif
         }
 
+        private static bool IsNet5SingleFileApp()
+        {
+            // Use a heuristic that GetAssemblyDirectory() will return null for single file apps.
+            return PlatformApis.IsNet5OrHigher && GetAssemblyDirectory() == null;
+        }
+
 #if !NETSTANDARD
         private static bool IsFileUri(string uri)
         {