1 #region Copyright notice and license 2 3 // Copyright 2015 gRPC authors. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 #endregion 18 19 using System; 20 using System.IO; 21 using System.Reflection; 22 23 using Grpc.Core.Logging; 24 25 namespace Grpc.Core.Internal 26 { 27 /// <summary> 28 /// Takes care of loading C# native extension and provides access to PInvoke calls the library exports. 29 /// </summary> 30 internal sealed class NativeExtension 31 { 32 // Enviroment variable can be used to force loading the native extension from given location. 33 private const string CsharpExtOverrideLocationEnvVarName = "GRPC_CSHARP_EXT_OVERRIDE_LOCATION"; 34 static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<NativeExtension>(); 35 static readonly object staticLock = new object(); 36 static volatile NativeExtension instance; 37 38 readonly NativeMethods nativeMethods; 39 NativeExtension()40 private NativeExtension() 41 { 42 this.nativeMethods = LoadNativeMethods(); 43 44 // Redirect the native logs as the very first thing after loading the native extension 45 // to make sure we don't lose any logs. 46 NativeLogRedirector.Redirect(this.nativeMethods); 47 48 // Initialize 49 NativeCallbackDispatcher.Init(this.nativeMethods); 50 51 DefaultSslRootsOverride.Override(this.nativeMethods); 52 53 Logger.Debug("gRPC native library loaded successfully."); 54 } 55 56 /// <summary> 57 /// Gets singleton instance of this class. 58 /// The native extension is loaded when called for the first time. 59 /// </summary> Get()60 public static NativeExtension Get() 61 { 62 if (instance == null) 63 { 64 lock (staticLock) 65 { 66 if (instance == null) { 67 instance = new NativeExtension(); 68 } 69 } 70 } 71 return instance; 72 } 73 74 /// <summary> 75 /// Provides access to the exported native methods. 76 /// </summary> 77 public NativeMethods NativeMethods 78 { 79 get { return this.nativeMethods; } 80 } 81 82 /// <summary> 83 /// Detects which configuration of native extension to load and explicitly loads the dynamic library. 84 /// The explicit load makes sure that we can detect any loading problems early on. 85 /// </summary> LoadNativeMethodsUsingExplicitLoad()86 private static NativeMethods LoadNativeMethodsUsingExplicitLoad() 87 { 88 // NOTE: a side effect of searching the native extension's library file relatively to the assembly location is that when Grpc.Core assembly 89 // is loaded via reflection from a different app's context, the native extension is still loaded correctly 90 // (while if we used [DllImport], the native extension won't be on the other app's search path for shared libraries). 91 var assemblyDirectory = GetAssemblyDirectory(); 92 93 // With "classic" VS projects, the native libraries get copied using a .targets rule to the build output folder 94 // alongside the compiled assembly. 95 // With dotnet SDK projects targeting net45 framework, the native libraries (just the required ones) 96 // are similarly copied to the built output folder, through the magic of Microsoft.NETCore.Platforms. 97 var classicPath = Path.Combine(assemblyDirectory, GetNativeLibraryFilename()); 98 99 // With dotnet SDK project targeting netcoreappX.Y, projects will use Grpc.Core assembly directly in the location where it got restored 100 // by nuget. We locate the native libraries based on known structure of Grpc.Core nuget package. 101 // When "dotnet publish" is used, the runtimes directory is copied next to the published assemblies. 102 string runtimesDirectory = string.Format("runtimes/{0}/native", GetRuntimeIdString()); 103 var netCorePublishedAppStylePath = Path.Combine(assemblyDirectory, runtimesDirectory, GetNativeLibraryFilename()); 104 var netCoreAppStylePath = Path.Combine(assemblyDirectory, "../..", runtimesDirectory, GetNativeLibraryFilename()); 105 106 // Look for the native library in all possible locations in given order. 107 string[] paths = new[] { classicPath, netCorePublishedAppStylePath, netCoreAppStylePath}; 108 109 // The UnmanagedLibrary mechanism for loading the native extension while avoiding 110 // direct use of DllImport is quite complicated but it is currently needed to ensure: 111 // 1.) the native extension is loaded eagerly (needed to avoid startup issues) 112 // 2.) less common scenarios (such as loading Grpc.Core.dll by reflection) still work 113 // 3.) loading native extension from an arbitrary location when set by an enviroment variable 114 // TODO(jtattermusch): revisit the possibility of eliminating UnmanagedLibrary completely in the future. 115 return new NativeMethods(new UnmanagedLibrary(paths)); 116 } 117 118 /// <summary> 119 /// Loads native methods using the <c>[DllImport(LIBRARY_NAME)]</c> attributes. 120 /// Note that this way of loading the native extension is "lazy" and doesn't 121 /// detect any "missing library" problems until we actually try to invoke the native methods 122 /// (which could be too late and could cause weird hangs at startup) 123 /// </summary> LoadNativeMethodsUsingDllImports()124 private static NativeMethods LoadNativeMethodsUsingDllImports() 125 { 126 // While in theory, we could just use [DllImport("grpc_csharp_ext")] for all the platforms 127 // and operating systems, the native libraries in the nuget package 128 // need to be laid out in a way that still allows things to work well under 129 // the legacy .NET Framework (where native libraries are a concept unknown to the runtime). 130 // Therefore, we use several flavors of the DllImport attribute 131 // (e.g. the ".x86" vs ".x64" suffix) and we choose the one we want at runtime. 132 // The classes with the list of DllImport'd methods are code generated, 133 // so having more than just one doesn't really bother us. 134 135 // on Windows, the DllImport("grpc_csharp_ext.x64") doesn't work 136 // but DllImport("grpc_csharp_ext.x64.dll") does, so we need a special case for that. 137 // See https://github.com/dotnet/coreclr/pull/17505 (fixed in .NET Core 3.1+) 138 bool useDllSuffix = PlatformApis.IsWindows; 139 if (PlatformApis.ProcessArchitecture == CommonPlatformDetection.CpuArchitecture.X64) 140 { 141 if (useDllSuffix) 142 { 143 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x64_dll()); 144 } 145 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x64()); 146 } 147 else if (PlatformApis.ProcessArchitecture == CommonPlatformDetection.CpuArchitecture.X86) 148 { 149 if (useDllSuffix) 150 { 151 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x86_dll()); 152 } 153 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_x86()); 154 } 155 else if (PlatformApis.ProcessArchitecture == CommonPlatformDetection.CpuArchitecture.Arm64) 156 { 157 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib_arm64()); 158 } 159 else 160 { 161 throw new InvalidOperationException($"Unsupported architecture \"{PlatformApis.ProcessArchitecture}\"."); 162 } 163 } 164 165 /// <summary> 166 /// Loads native extension and return native methods delegates. 167 /// </summary> LoadNativeMethods()168 private static NativeMethods LoadNativeMethods() 169 { 170 if (PlatformApis.IsUnity) 171 { 172 return LoadNativeMethodsUnity(); 173 } 174 if (PlatformApis.IsXamarin) 175 { 176 return LoadNativeMethodsXamarin(); 177 } 178 179 // Override location of grpc_csharp_ext native library with an environment variable 180 // Use at your own risk! By doing this you take all the responsibility that the dynamic library 181 // is of the correct version (needs to match the Grpc.Core assembly exactly) and of the correct platform/architecture. 182 var nativeExtPathFromEnv = System.Environment.GetEnvironmentVariable(CsharpExtOverrideLocationEnvVarName); 183 if (!string.IsNullOrEmpty(nativeExtPathFromEnv)) 184 { 185 return new NativeMethods(new UnmanagedLibrary(new string[] { nativeExtPathFromEnv })); 186 } 187 188 if (IsNet5SingleFileApp()) 189 { 190 // Ideally we'd want to always load the native extension explicitly 191 // (to detect any potential problems early on and to avoid hard-to-debug startup issues) 192 // but the mechanism we normally use doesn't work when running 193 // as a single file app (see https://github.com/grpc/grpc/pull/24744). 194 // Therefore in this case we simply rely 195 // on the automatic [DllImport] loading logic to do the right thing. 196 return LoadNativeMethodsUsingDllImports(); 197 } 198 return LoadNativeMethodsUsingExplicitLoad(); 199 } 200 201 /// <summary> 202 /// Return native method delegates when running on Unity platform. 203 /// Unity does not use standard NuGet packages and the native library is treated 204 /// there as a "native plugin" which is (provided it has the right metadata) 205 /// automatically made available to <c>[DllImport]</c> loading logic. 206 /// WARNING: Unity support is experimental and work-in-progress. Don't expect it to work. 207 /// </summary> LoadNativeMethodsUnity()208 private static NativeMethods LoadNativeMethodsUnity() 209 { 210 if (PlatformApis.IsUnityIOS) 211 { 212 return new NativeMethods(new NativeMethods.DllImportsFromStaticLib()); 213 } 214 // most other platforms load unity plugins as a shared library 215 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib()); 216 } 217 218 /// <summary> 219 /// Return native method delegates when running on the Xamarin platform. 220 /// On Xamarin, the standard <c>[DllImport]</c> loading logic just works 221 /// as the native library metadata is provided by the <c>AndroidNativeLibrary</c> or 222 /// <c>NativeReference</c> items in the Xamarin projects (injected automatically 223 /// by the Grpc.Core.Xamarin nuget). 224 /// WARNING: Xamarin support is experimental and work-in-progress. Don't expect it to work. 225 /// </summary> LoadNativeMethodsXamarin()226 private static NativeMethods LoadNativeMethodsXamarin() 227 { 228 if (PlatformApis.IsXamarinAndroid) 229 { 230 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib()); 231 } 232 return new NativeMethods(new NativeMethods.DllImportsFromStaticLib()); 233 } 234 GetAssemblyDirectory()235 private static string GetAssemblyDirectory() 236 { 237 var assembly = typeof(NativeExtension).GetTypeInfo().Assembly; 238 #if NETSTANDARD 239 // Assembly.EscapedCodeBase does not exist under CoreCLR, but assemblies imported from a nuget package 240 // don't seem to be shadowed by DNX-based projects at all. 241 var assemblyLocation = assembly.Location; 242 if (string.IsNullOrEmpty(assemblyLocation)) 243 { 244 // In .NET5 single-file deployments, assembly.Location won't be available 245 // and we can use it for detecting whether we are running as a single file app. 246 // Also see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#other-considerations 247 return null; 248 } 249 return Path.GetDirectoryName(assemblyLocation); 250 #else 251 // If assembly is shadowed (e.g. in a webapp), EscapedCodeBase is pointing 252 // to the original location of the assembly, and Location is pointing 253 // to the shadow copy. We care about the original location because 254 // the native dlls don't get shadowed. 255 256 var escapedCodeBase = assembly.EscapedCodeBase; 257 if (IsFileUri(escapedCodeBase)) 258 { 259 return Path.GetDirectoryName(new Uri(escapedCodeBase).LocalPath); 260 } 261 return Path.GetDirectoryName(assembly.Location); 262 #endif 263 } 264 IsNet5SingleFileApp()265 private static bool IsNet5SingleFileApp() 266 { 267 // Use a heuristic that GetAssemblyDirectory() will return null for single file apps. 268 return PlatformApis.IsNet5OrHigher && GetAssemblyDirectory() == null; 269 } 270 271 #if !NETSTANDARD IsFileUri(string uri)272 private static bool IsFileUri(string uri) 273 { 274 return uri.ToLowerInvariant().StartsWith(Uri.UriSchemeFile); 275 } 276 #endif 277 GetRuntimeIdString()278 private static string GetRuntimeIdString() 279 { 280 string architecture = GetArchitectureString(); 281 if (PlatformApis.IsWindows) 282 { 283 return string.Format("win-{0}", architecture); 284 } 285 if (PlatformApis.IsLinux) 286 { 287 return string.Format("linux-{0}", architecture); 288 } 289 if (PlatformApis.IsMacOSX) 290 { 291 return string.Format("osx-{0}", architecture); 292 } 293 throw new InvalidOperationException("Unsupported platform."); 294 } 295 GetArchitectureString()296 private static string GetArchitectureString() 297 { 298 switch (PlatformApis.ProcessArchitecture) 299 { 300 case CommonPlatformDetection.CpuArchitecture.X86: 301 return "x86"; 302 case CommonPlatformDetection.CpuArchitecture.X64: 303 return "x64"; 304 case CommonPlatformDetection.CpuArchitecture.Arm64: 305 return "arm64"; 306 default: 307 throw new InvalidOperationException($"Unsupported architecture \"{PlatformApis.ProcessArchitecture}\"."); 308 } 309 } 310 311 // platform specific file name of the extension library GetNativeLibraryFilename()312 private static string GetNativeLibraryFilename() 313 { 314 string architecture = GetArchitectureString(); 315 if (PlatformApis.IsWindows) 316 { 317 return string.Format("grpc_csharp_ext.{0}.dll", architecture); 318 } 319 if (PlatformApis.IsLinux) 320 { 321 return string.Format("libgrpc_csharp_ext.{0}.so", architecture); 322 } 323 if (PlatformApis.IsMacOSX) 324 { 325 return string.Format("libgrpc_csharp_ext.{0}.dylib", architecture); 326 } 327 throw new InvalidOperationException("Unsupported platform."); 328 } 329 } 330 } 331