• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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