Skip to content

Commit 8f6ad7c

Browse files
authored
Merge pull request #1563 from tmat/NativeLoad
Avoid reading and writing global state when loading native library
2 parents 76b997a + 497f91f commit 8f6ad7c

14 files changed

+375
-83
lines changed

LibGit2Sharp.Tests/GlobalSettingsFixture.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using System.Text.RegularExpressions;
1+
using System;
2+
using System.IO;
3+
using System.Text.RegularExpressions;
4+
using LibGit2Sharp.Core;
25
using LibGit2Sharp.Tests.TestHelpers;
36
using Xunit;
47

@@ -49,5 +52,34 @@ public void TryingToResetNativeLibraryPathAfterLoadedThrows()
4952

5053
Assert.Throws<LibGit2SharpException>(() => { GlobalSettings.NativeLibraryPath = "C:/Foo"; });
5154
}
55+
56+
[SkippableTheory]
57+
[InlineData("x86")]
58+
[InlineData("x64")]
59+
public void LoadFromSpecifiedPath(string architecture)
60+
{
61+
Skip.IfNot(Platform.IsRunningOnNetFramework(), ".NET Framework only test.");
62+
63+
var nativeDllFileName = NativeDllName.Name + ".dll";
64+
var testDir = Path.GetDirectoryName(typeof(GlobalSettingsFixture).Assembly.Location);
65+
var testAppExe = Path.Combine(testDir, $"NativeLibraryLoadTestApp.{architecture}.exe");
66+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
67+
var platformDir = Path.Combine(tempDir, "plat");
68+
69+
try
70+
{
71+
Directory.CreateDirectory(Path.Combine(platformDir, architecture));
72+
File.Copy(Path.Combine(GlobalSettings.NativeLibraryPath, architecture, nativeDllFileName), Path.Combine(platformDir, architecture, nativeDllFileName));
73+
74+
var (output, exitCode) = ProcessHelper.RunProcess(testAppExe, arguments: $@"{NativeDllName.Name} ""{platformDir}""", workingDirectory: tempDir);
75+
76+
Assert.Empty(output);
77+
Assert.Equal(0, exitCode);
78+
}
79+
finally
80+
{
81+
DirectoryHelper.DeleteDirectory(tempDir);
82+
}
83+
}
5284
}
5385
}

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
<ItemGroup>
99
<ProjectReference Include="..\LibGit2Sharp\LibGit2Sharp.csproj" />
10+
<ProjectReference Include="..\NativeLibraryLoadTestApp\x86\NativeLibraryLoadTestApp.x86.csproj" Condition="'$(TargetFramework)' == 'net461'" ReferenceOutputAssembly="false" OutputItemType="TestAppExe" />
11+
<ProjectReference Include="..\NativeLibraryLoadTestApp\x64\NativeLibraryLoadTestApp.x64.csproj" Condition="'$(TargetFramework)' == 'net461'" ReferenceOutputAssembly="false" OutputItemType="TestAppExe" />
1012
</ItemGroup>
1113

1214
<ItemGroup>
@@ -24,4 +26,18 @@
2426
<Compile Remove="desktop\**" Condition=" '$(TargetFramework)' != 'net461' " />
2527
</ItemGroup>
2628

29+
<Target Name="CopyTestAppExes" AfterTargets="ResolveProjectReferences">
30+
<ItemGroup>
31+
<_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).exe')" />
32+
<_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).exe.config')" />
33+
<_TestAppFile Include="@(TestAppExe->'%(RootDir)%(Directory)%(Filename).pdb')" />
34+
</ItemGroup>
35+
36+
<ItemGroup>
37+
<Content Include="@(_TestAppFile)" CopyToOutputDirectory="PreserveNewest" Visible="false" />
38+
</ItemGroup>
39+
</Target>
40+
41+
<Import Project="..\Targets\GenerateNativeDllName.targets" />
42+
2743
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Text;
5+
6+
namespace LibGit2Sharp.Tests
7+
{
8+
public static class ProcessHelper
9+
{
10+
public static (string, int) RunProcess(string fileName, string arguments, string workingDirectory = null)
11+
{
12+
var process = new Process
13+
{
14+
StartInfo = new ProcessStartInfo(fileName, arguments)
15+
{
16+
RedirectStandardError = true,
17+
RedirectStandardOutput = true,
18+
CreateNoWindow = true,
19+
UseShellExecute = false,
20+
WorkingDirectory = workingDirectory ?? string.Empty
21+
}
22+
};
23+
24+
var output = new StringBuilder();
25+
26+
process.OutputDataReceived += (_, e) => output.AppendLine(e.Data);
27+
process.ErrorDataReceived += (_, e) => output.AppendLine(e.Data);
28+
29+
process.Start();
30+
31+
process.WaitForExit();
32+
33+
return (output.ToString(), process.ExitCode);
34+
}
35+
}
36+
}

LibGit2Sharp.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ EndProject
99
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0CA739FD-DA4D-4F64-9834-DA14A3ECD04B}"
1010
ProjectSection(SolutionItems) = preProject
1111
.gitignore = .gitignore
12+
Targets\CodeGenerator.targets = Targets\CodeGenerator.targets
1213
Directory.Build.props = Directory.Build.props
1314
Directory.Build.targets = Directory.Build.targets
15+
Targets\GenerateNativeDllName.targets = Targets\GenerateNativeDllName.targets
1416
nuget.config = nuget.config
1517
version.json = version.json
1618
EndProjectSection
1719
EndProject
20+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeLibraryLoadTestApp.x86", "NativeLibraryLoadTestApp\x86\NativeLibraryLoadTestApp.x86.csproj", "{86453D2C-4953-4DF4-B12A-ADE579608BAA}"
21+
EndProject
22+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeLibraryLoadTestApp.x64", "NativeLibraryLoadTestApp\x64\NativeLibraryLoadTestApp.x64.csproj", "{5C55175D-6A1F-4C51-B791-BF7DD00124EE}"
23+
EndProject
1824
Global
1925
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2026
Debug|Any CPU = Debug|Any CPU
@@ -29,6 +35,14 @@ Global
2935
{286E63EB-04DD-4ADE-88D6-041B57800761}.Debug|Any CPU.Build.0 = Debug|Any CPU
3036
{286E63EB-04DD-4ADE-88D6-041B57800761}.Release|Any CPU.ActiveCfg = Release|Any CPU
3137
{286E63EB-04DD-4ADE-88D6-041B57800761}.Release|Any CPU.Build.0 = Release|Any CPU
38+
{86453D2C-4953-4DF4-B12A-ADE579608BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39+
{86453D2C-4953-4DF4-B12A-ADE579608BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
40+
{86453D2C-4953-4DF4-B12A-ADE579608BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{86453D2C-4953-4DF4-B12A-ADE579608BAA}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43+
{5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
44+
{5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
45+
{5C55175D-6A1F-4C51-B791-BF7DD00124EE}.Release|Any CPU.Build.0 = Release|Any CPU
3246
EndGlobalSection
3347
GlobalSection(SolutionProperties) = preSolution
3448
HideSolutionNode = FALSE

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
using System;
2-
using System.Globalization;
32
using System.IO;
43
using System.Runtime.CompilerServices;
4+
using System.Runtime.ConstrainedExecution;
55
using System.Runtime.InteropServices;
66
using LibGit2Sharp.Core.Handles;
77

8+
// Restrict the set of directories where the native library is loaded from to safe directories.
9+
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.ApplicationDirectory | DllImportSearchPath.SafeDirectories)]
10+
11+
#pragma warning disable IDE1006 // Naming Styles
12+
813
// ReSharper disable InconsistentNaming
914
namespace LibGit2Sharp.Core
1015
{
@@ -17,41 +22,68 @@ internal static class NativeMethods
1722
// This will handle initialization and shutdown of the underlying
1823
// native library.
1924
#pragma warning disable 0414
20-
private static readonly NativeShutdownObject shutdownObject;
21-
#pragma warning restore 0414
25+
private static NativeShutdownObject shutdownObject;
26+
#pragma warning restore 0414
2227

2328
static NativeMethods()
2429
{
25-
if (Platform.OperatingSystem == OperatingSystemType.Windows)
30+
if (Platform.IsRunningOnNetFramework() || Platform.IsRunningOnNetCore())
2631
{
27-
string nativeLibraryPath = GlobalSettings.GetAndLockNativeLibraryPath();
28-
29-
string path = Path.Combine(nativeLibraryPath, Platform.ProcessorArchitecture);
30-
31-
const string pathEnvVariable = "PATH";
32-
Environment.SetEnvironmentVariable(pathEnvVariable,
33-
String.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", path, Path.PathSeparator, Environment.GetEnvironmentVariable(pathEnvVariable)));
32+
string nativeLibraryDir = GlobalSettings.GetAndLockNativeLibraryPath();
33+
if (nativeLibraryDir != null)
34+
{
35+
string nativeLibraryPath = Path.Combine(nativeLibraryDir, libgit2 + Platform.GetNativeLibraryExtension());
36+
37+
// Try to load the .dll from the path explicitly.
38+
// If this call succeeds further DllImports will find the library loaded and not attempt to load it again.
39+
// If it fails the next DllImport will load the library from safe directories.
40+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
41+
{
42+
LoadWindowsLibrary(nativeLibraryPath);
43+
}
44+
else
45+
{
46+
LoadUnixLibrary(nativeLibraryPath, RTLD_NOW);
47+
}
48+
}
3449
}
3550

36-
LoadNativeLibrary();
37-
shutdownObject = new NativeShutdownObject();
51+
InitializeNativeLibrary();
3852
}
3953

54+
public const int RTLD_NOW = 0x002;
55+
56+
[DllImport("libdl", EntryPoint = "dlopen")]
57+
private static extern IntPtr LoadUnixLibrary(string path, int flags);
58+
59+
[DllImport("kernel32", EntryPoint = "LoadLibrary")]
60+
private static extern IntPtr LoadWindowsLibrary(string path);
61+
4062
// Avoid inlining this method because otherwise mono's JITter may try
4163
// to load the library _before_ we've configured the path.
4264
[MethodImpl(MethodImplOptions.NoInlining)]
43-
private static void LoadNativeLibrary()
65+
private static void InitializeNativeLibrary()
4466
{
45-
// Configure the OpenSSL locking on the true initialization
46-
// of the library.
47-
if (git_libgit2_init() == 1)
67+
int initCounter;
68+
try
69+
{
70+
}
71+
finally // avoid thread aborts
72+
{
73+
// Initialization can be called multiple times as long as there is a corresponding shutdown to each initialization.
74+
initCounter = git_libgit2_init();
75+
shutdownObject = new NativeShutdownObject();
76+
}
77+
78+
// Configure the OpenSSL locking on the first initialization of the library in the current process.
79+
if (initCounter == 1)
4880
{
4981
git_openssl_set_locking();
5082
}
5183
}
5284

5385
// Shutdown the native library in a finalizer.
54-
private sealed class NativeShutdownObject
86+
private sealed class NativeShutdownObject : CriticalFinalizerObject
5587
{
5688
~NativeShutdownObject()
5789
{

LibGit2Sharp/Core/Platform.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,43 @@ public static OperatingSystemType OperatingSystem
3333
return OperatingSystemType.MacOSX;
3434
}
3535

36-
throw new InvalidOperationException();
36+
throw new PlatformNotSupportedException();
3737
}
3838
}
39+
40+
public static string GetNativeLibraryExtension()
41+
{
42+
switch (OperatingSystem)
43+
{
44+
case OperatingSystemType.MacOSX:
45+
return ".dylib";
46+
47+
case OperatingSystemType.Unix:
48+
return ".so";
49+
50+
case OperatingSystemType.Windows:
51+
return ".dll";
52+
}
53+
54+
throw new PlatformNotSupportedException();
55+
}
56+
57+
/// <summary>
58+
/// Returns true if the runtime is Mono.
59+
/// </summary>
60+
public static bool IsRunningOnMono()
61+
=> Type.GetType("Mono.Runtime") != null;
62+
63+
/// <summary>
64+
/// Returns true if the runtime is .NET Framework.
65+
/// </summary>
66+
public static bool IsRunningOnNetFramework()
67+
=> typeof(object).Assembly.GetName().Name == "mscorlib" && !IsRunningOnMono();
68+
69+
/// <summary>
70+
/// Returns true if the runtime is .NET Core.
71+
/// </summary>
72+
public static bool IsRunningOnNetCore()
73+
=> typeof(object).Assembly.GetName().Name != "mscorlib";
3974
}
4075
}

0 commit comments

Comments
 (0)