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.Is64Bit) 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 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 } 156 157 /// <summary> 158 /// Loads native extension and return native methods delegates. 159 /// </summary> LoadNativeMethods()160 private static NativeMethods LoadNativeMethods() 161 { 162 if (PlatformApis.IsUnity) 163 { 164 return LoadNativeMethodsUnity(); 165 } 166 if (PlatformApis.IsXamarin) 167 { 168 return LoadNativeMethodsXamarin(); 169 } 170 171 // Override location of grpc_csharp_ext native library with an environment variable 172 // Use at your own risk! By doing this you take all the responsibility that the dynamic library 173 // is of the correct version (needs to match the Grpc.Core assembly exactly) and of the correct platform/architecture. 174 var nativeExtPathFromEnv = System.Environment.GetEnvironmentVariable(CsharpExtOverrideLocationEnvVarName); 175 if (!string.IsNullOrEmpty(nativeExtPathFromEnv)) 176 { 177 return new NativeMethods(new UnmanagedLibrary(new string[] { nativeExtPathFromEnv })); 178 } 179 180 if (IsNet5SingleFileApp()) 181 { 182 // Ideally we'd want to always load the native extension explicitly 183 // (to detect any potential problems early on and to avoid hard-to-debug startup issues) 184 // but the mechanism we normally use doesn't work when running 185 // as a single file app (see https://github.com/grpc/grpc/pull/24744). 186 // Therefore in this case we simply rely 187 // on the automatic [DllImport] loading logic to do the right thing. 188 return LoadNativeMethodsUsingDllImports(); 189 } 190 return LoadNativeMethodsUsingExplicitLoad(); 191 } 192 193 /// <summary> 194 /// Return native method delegates when running on Unity platform. 195 /// Unity does not use standard NuGet packages and the native library is treated 196 /// there as a "native plugin" which is (provided it has the right metadata) 197 /// automatically made available to <c>[DllImport]</c> loading logic. 198 /// WARNING: Unity support is experimental and work-in-progress. Don't expect it to work. 199 /// </summary> LoadNativeMethodsUnity()200 private static NativeMethods LoadNativeMethodsUnity() 201 { 202 if (PlatformApis.IsUnityIOS) 203 { 204 return new NativeMethods(new NativeMethods.DllImportsFromStaticLib()); 205 } 206 // most other platforms load unity plugins as a shared library 207 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib()); 208 } 209 210 /// <summary> 211 /// Return native method delegates when running on the Xamarin platform. 212 /// On Xamarin, the standard <c>[DllImport]</c> loading logic just works 213 /// as the native library metadata is provided by the <c>AndroidNativeLibrary</c> or 214 /// <c>NativeReference</c> items in the Xamarin projects (injected automatically 215 /// by the Grpc.Core.Xamarin nuget). 216 /// WARNING: Xamarin support is experimental and work-in-progress. Don't expect it to work. 217 /// </summary> LoadNativeMethodsXamarin()218 private static NativeMethods LoadNativeMethodsXamarin() 219 { 220 if (PlatformApis.IsXamarinAndroid) 221 { 222 return new NativeMethods(new NativeMethods.DllImportsFromSharedLib()); 223 } 224 return new NativeMethods(new NativeMethods.DllImportsFromStaticLib()); 225 } 226 GetAssemblyDirectory()227 private static string GetAssemblyDirectory() 228 { 229 var assembly = typeof(NativeExtension).GetTypeInfo().Assembly; 230 #if NETSTANDARD 231 // Assembly.EscapedCodeBase does not exist under CoreCLR, but assemblies imported from a nuget package 232 // don't seem to be shadowed by DNX-based projects at all. 233 var assemblyLocation = assembly.Location; 234 if (string.IsNullOrEmpty(assemblyLocation)) 235 { 236 // In .NET5 single-file deployments, assembly.Location won't be available 237 // and we can use it for detecting whether we are running as a single file app. 238 // Also see https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#other-considerations 239 return null; 240 } 241 return Path.GetDirectoryName(assemblyLocation); 242 #else 243 // If assembly is shadowed (e.g. in a webapp), EscapedCodeBase is pointing 244 // to the original location of the assembly, and Location is pointing 245 // to the shadow copy. We care about the original location because 246 // the native dlls don't get shadowed. 247 248 var escapedCodeBase = assembly.EscapedCodeBase; 249 if (IsFileUri(escapedCodeBase)) 250 { 251 return Path.GetDirectoryName(new Uri(escapedCodeBase).LocalPath); 252 } 253 return Path.GetDirectoryName(assembly.Location); 254 #endif 255 } 256 IsNet5SingleFileApp()257 private static bool IsNet5SingleFileApp() 258 { 259 // Use a heuristic that GetAssemblyDirectory() will return null for single file apps. 260 return PlatformApis.IsNet5OrHigher && GetAssemblyDirectory() == null; 261 } 262 263 #if !NETSTANDARD IsFileUri(string uri)264 private static bool IsFileUri(string uri) 265 { 266 return uri.ToLowerInvariant().StartsWith(Uri.UriSchemeFile); 267 } 268 #endif 269 GetRuntimeIdString()270 private static string GetRuntimeIdString() 271 { 272 string architecture = GetArchitectureString(); 273 if (PlatformApis.IsWindows) 274 { 275 return string.Format("win-{0}", architecture); 276 } 277 if (PlatformApis.IsLinux) 278 { 279 return string.Format("linux-{0}", architecture); 280 } 281 if (PlatformApis.IsMacOSX) 282 { 283 return string.Format("osx-{0}", architecture); 284 } 285 throw new InvalidOperationException("Unsupported platform."); 286 } 287 288 // Currently, only Intel platform is supported. GetArchitectureString()289 private static string GetArchitectureString() 290 { 291 if (PlatformApis.Is64Bit) 292 { 293 return "x64"; 294 } 295 else 296 { 297 return "x86"; 298 } 299 } 300 301 // platform specific file name of the extension library GetNativeLibraryFilename()302 private static string GetNativeLibraryFilename() 303 { 304 string architecture = GetArchitectureString(); 305 if (PlatformApis.IsWindows) 306 { 307 return string.Format("grpc_csharp_ext.{0}.dll", architecture); 308 } 309 if (PlatformApis.IsLinux) 310 { 311 return string.Format("libgrpc_csharp_ext.{0}.so", architecture); 312 } 313 if (PlatformApis.IsMacOSX) 314 { 315 return string.Format("libgrpc_csharp_ext.{0}.dylib", architecture); 316 } 317 throw new InvalidOperationException("Unsupported platform."); 318 } 319 } 320 } 321