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