Skip to content
124 changes: 118 additions & 6 deletions sources/ClangSharp.Interop/clang.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
Expand All @@ -14,6 +15,9 @@ public static unsafe partial class @clang
{
public static event DllImportResolver? ResolveLibrary;

public const int MajorVersion = 21;
public const int MinorVersion = 1;

static @clang()
{
if (!Configuration.DisableResolveLibraryHook)
Expand All @@ -24,29 +28,137 @@ static @clang()

private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
// Let user hooks attempt to resolve first, so they can override the behavior for their app
if (TryResolveLibrary(libraryName, assembly, searchPath, out var nativeLibrary))
{
return nativeLibrary;
}

if (libraryName.Equals("libclang", StringComparison.Ordinal) && TryResolveClang(assembly, searchPath, out nativeLibrary))
// Then try the normal resolution of the library
if (TryResolve(libraryName, assembly, searchPath.GetValueOrDefault(), out nativeLibrary))
{
return nativeLibrary;
}

return IntPtr.Zero;
// Finally, do library specific resolution as applicable
if (libraryName.Equals("libclang", StringComparison.Ordinal))
{
if (TryResolveClang(assembly, searchPath, out nativeLibrary))
{
return nativeLibrary;
}
}

return default;
}

private static bool TryResolve(string libraryName, Assembly assembly, DllImportSearchPath searchPath, out IntPtr nativeLibrary)
{
if (TryResolveFromAppDirectory(libraryName, assembly, searchPath, out nativeLibrary))
{
return true;
}

return NativeLibrary.TryLoad(libraryName, assembly, searchPath, out nativeLibrary);
}

private static bool TryResolveClang(Assembly assembly, DllImportSearchPath? searchPath, out IntPtr nativeLibrary)
Comment thread
tannergooding marked this conversation as resolved.
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return NativeLibrary.TryLoad("libclang.so.20", assembly, searchPath, out nativeLibrary)
|| NativeLibrary.TryLoad("libclang-20", assembly, searchPath, out nativeLibrary)
|| NativeLibrary.TryLoad("libclang.so.1", assembly, searchPath, out nativeLibrary);
return TryResolve($"libclang.so.{MajorVersion}", assembly, searchPath.GetValueOrDefault(), out nativeLibrary)
|| TryResolve($"libclang-{MajorVersion}", assembly, searchPath.GetValueOrDefault(), out nativeLibrary)
|| TryResolve("libclang.so.1", assembly, searchPath.GetValueOrDefault(), out nativeLibrary);
}

nativeLibrary = default;
return false;
}

private static bool TryResolveFromAppDirectory(string libraryName, Assembly assembly, DllImportSearchPath searchPath, out IntPtr nativeLibrary)
{
nativeLibrary = default;

// DllImportSearchPath is ignored on non-Windows and the default resolution
// algorithm does not look in the application directory.
// We need to look here for any dependencies that are shipped SxS with the
// application, using the standard variations allowed.
if (OperatingSystem.IsWindows())
{
return false;
}

if (((searchPath & (DllImportSearchPath.SafeDirectories | DllImportSearchPath.ApplicationDirectory)) == 0))
{
return false;
}

if (Path.IsPathFullyQualified(libraryName))
{
return false;
}

// We use Environment.ProcessPath because that is the location of the executable
// that launched the process. AppContext.BaseDirectory can be overridden and is
// rather more the "AssemblyDirectory". This is a subtle distinction, but an important one.
var applicationDirectory = Path.GetDirectoryName(Environment.ProcessPath);

if (applicationDirectory is null)
{
return false;
}

var libraryPath = Path.Combine(applicationDirectory, libraryName);

if (NativeLibrary.TryLoad(libraryPath, assembly, searchPath, out nativeLibrary))
{
return true;
}

// We try the given name first, as that is always preferred. Then we try, in order:
// * libname
// * name.ext
// * libname.ext
//
// This can result in some redundant lookups, but it's how the built-in logic works
// for non-relative paths.
var prefixedLibraryName = "";

if (!libraryName.Contains(Path.DirectorySeparatorChar, StringComparison.Ordinal))
{
prefixedLibraryName = $"lib{libraryName}";
libraryPath = Path.Combine(applicationDirectory, prefixedLibraryName);

if (NativeLibrary.TryLoad(libraryPath, assembly, searchPath, out nativeLibrary))
{
return true;
}
}

var extension = ".so";

if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS())
{
extension = ".dylib";
}

libraryPath = Path.Combine(applicationDirectory, $"{libraryName}{extension}");

if (NativeLibrary.TryLoad(libraryPath, assembly, searchPath, out nativeLibrary))
{
return true;
}

if (prefixedLibraryName.Length != 0)
{
libraryPath = Path.Combine(applicationDirectory, $"{prefixedLibraryName}{extension}");

if (NativeLibrary.TryLoad(libraryPath, assembly, searchPath, out nativeLibrary))
{
return true;
}
}

nativeLibrary = IntPtr.Zero;
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ public sealed partial class PInvokeGenerator : IDisposable
private static readonly string[] s_doubleColonSeparator = ["::"];
private static readonly char[] s_doubleQuoteSeparator = ['"'];

private const string ExpectedClangVersion = "version 21.1";
private const string ExpectedClangSharpVersion = "version 21.1";
private static string ExpectedClangVersion => $"version {clang.MajorVersion}.{clang.MinorVersion}";
private static string ExpectedClangSharpVersion => ExpectedClangVersion; // change if necessary

private readonly CXIndex _index;
private readonly OutputBuilderFactory _outputBuilderFactory;
Expand Down
28 changes: 28 additions & 0 deletions tests/ClangSharp.UnitTests/ClangVersionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. Licensed under the MIT License (MIT). See License.md in the repository root for more information.

using System.Globalization;
using System.Text.RegularExpressions;
using ClangSharp.Interop;
using NUnit.Framework;

namespace ClangSharp.UnitTests;

public sealed class ClangVersionTest
{
[Test]
public void MajorMinorMatchesExpected()
{
using var versionString = clang.getClangVersion();
var versionText = versionString.ToString();

var match = Regex.Match(versionText, @"version (\d+)\.(\d+)");

Assert.That(match.Success, Is.True, $"Could not parse version from: {versionText}");

var major = int.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture);
var minor = int.Parse(match.Groups[2].ValueSpan, CultureInfo.InvariantCulture);

Assert.That(major, Is.EqualTo(clang.MajorVersion), $"libclang major version mismatch. Full version string: {versionText}");
Assert.That(minor, Is.EqualTo(clang.MinorVersion), $"libclang minor version mismatch. Full version string: {versionText}");
}
}
Loading