From 7f99dd25c6df25120876684af0d5fd946693b89a Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Fri, 3 Jul 2026 02:15:37 +0800 Subject: [PATCH 1/6] Fix libClang resolution on Linux: remove stale hardcoded version The TryResolveClang method on Linux tried versioned SONAME names (libclang.so.20, libclang-20) that were hardcoded for LLVM 20. ClangSharp 21.1.8.3 is built for LLVM 21, so these names never match. The hardcoded version number gets stale with every LLVM major release. Remove the version-specific names entirely and try only version-agnostic names: - libclang.so (finds co-located binary in RID packages or system symlink) - libclang.so.1 (Debian/Ubuntu runtime SONAME from libclang1 package) This fixes ClangSharpPInvokeGenerator when running as a dotnet tool from a RID-specific package (e.g. ClangSharpPInvokeGenerator.linux-x64), where libclang.so is placed alongside the executable. On Linux, dlopen does not search the executable's directory by default, but the SafeDirectories DllImportSearchPath adds it, allowing NativeLibrary.TryLoad to find the co-located libclang.so. Relates to #586. --- sources/ClangSharp.Interop/clang.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index 037ab5b1..bd59b0c2 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -41,8 +41,7 @@ private static bool TryResolveClang(Assembly assembly, DllImportSearchPath? sear { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - return NativeLibrary.TryLoad("libclang.so.20", assembly, searchPath, out nativeLibrary) - || NativeLibrary.TryLoad("libclang-20", assembly, searchPath, out nativeLibrary) + return NativeLibrary.TryLoad("libclang.so", assembly, searchPath, out nativeLibrary) || NativeLibrary.TryLoad("libclang.so.1", assembly, searchPath, out nativeLibrary); } From 7656eb1fdc1620b624aeed319f2355c6eecf6eef Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Fri, 3 Jul 2026 16:35:59 +0800 Subject: [PATCH 2/6] Remove TryResolveClang entirely - let default resolver handle Linux The TryResolveClang method was a Linux-specific fallback that tried versioned SONAMEs (libclang.so.20, libclang-20, libclang.so.1) before the default resolver could run. This was problematic: 1. The version number (20) was hardcoded and didn't match ClangSharp 21.x (built for LLVM 21). This caused version mismatch errors when a system libclang-20 was found instead of v21. 2. The method was unnecessary: when it returned IntPtr.Zero, the default runtime resolver with SafeDirectories would find libclang.so anyway, the same way it finds libclang.dll on Windows (which has no special handling). 3. The versioned SONAME approach is inherently fragile - it breaks with every LLVM major version bump and different distros use different naming conventions. Removing the method entirely lets the default resolver handle all platforms uniformly. The DllImportResolver still supports user-provided resolvers via the ResolveLibrary event for advanced scenarios. Relates to #586. --- sources/ClangSharp.Interop/clang.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index bd59b0c2..fa1e6fe6 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -29,26 +29,9 @@ private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImpo return nativeLibrary; } - if (libraryName.Equals("libclang", StringComparison.Ordinal) && TryResolveClang(assembly, searchPath, out nativeLibrary)) - { - return nativeLibrary; - } - return IntPtr.Zero; } - private static bool TryResolveClang(Assembly assembly, DllImportSearchPath? searchPath, out IntPtr nativeLibrary) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return NativeLibrary.TryLoad("libclang.so", assembly, searchPath, out nativeLibrary) - || NativeLibrary.TryLoad("libclang.so.1", assembly, searchPath, out nativeLibrary); - } - - nativeLibrary = IntPtr.Zero; - return false; - } - private static bool TryResolveLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath, out IntPtr nativeLibrary) { var resolveLibrary = ResolveLibrary; From 24f2d57464d4752fe10a0408e767804da3da412d Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Fri, 3 Jul 2026 16:50:00 +0800 Subject: [PATCH 3/6] Explicitly search assembly directory for native libraries The default resolver with SafeDirectories should search the assembly's directory, but this doesn't work reliably for dotnet tools (especially AOT-compiled executables run from the NuGet cache via 'dotnet tool run'). This caused both libclang and libClangSharp to not be found on Linux, even though they are co-located with the executable in the RID package. Fix: After the user-provided resolver event, explicitly try loading from the assembly's own directory using Path.GetDirectoryName(assembly.Location). This handles all native dependencies (libclang, libClangSharp) uniformly on all platforms, without needing versioned SONAME fallbacks or LD_LIBRARY_PATH workarounds. --- sources/ClangSharp.Interop/clang.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index fa1e6fe6..98b49415 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -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; @@ -29,6 +30,18 @@ private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImpo return nativeLibrary; } + // The default resolver with SafeDirectories should search the assembly's directory, + // but this doesn't always work for dotnet tools (especially AOT-compiled executables + // run from the NuGet cache). Explicitly try the assembly's own directory as a fallback. + var assemblyDir = Path.GetDirectoryName(assembly.Location); + if (assemblyDir is not null) + { + if (NativeLibrary.TryLoad(Path.Combine(assemblyDir, libraryName), out nativeLibrary)) + { + return nativeLibrary; + } + } + return IntPtr.Zero; } From a3f78c0d0f8f53596a65158b99e4eab122710ef0 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Fri, 3 Jul 2026 22:24:25 +0800 Subject: [PATCH 4/6] Fix directory usage --- sources/ClangSharp.Interop/clang.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index 98b49415..d365f9a2 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -30,16 +30,10 @@ private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImpo return nativeLibrary; } - // The default resolver with SafeDirectories should search the assembly's directory, - // but this doesn't always work for dotnet tools (especially AOT-compiled executables - // run from the NuGet cache). Explicitly try the assembly's own directory as a fallback. - var assemblyDir = Path.GetDirectoryName(assembly.Location); - if (assemblyDir is not null) + // When invoked as a dotnet tool (ClangSharpPInvokeGenerator) on Unix, assemblies next to the executable aren't searched by default. + if (NativeLibrary.TryLoad(Path.Combine(AppContext.BaseDirectory, libraryName), out nativeLibrary)) { - if (NativeLibrary.TryLoad(Path.Combine(assemblyDir, libraryName), out nativeLibrary)) - { - return nativeLibrary; - } + return nativeLibrary; } return IntPtr.Zero; From 9335046ba39599f68285bc7eb2c95517ccbbbdb2 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Sat, 4 Jul 2026 00:40:52 +0800 Subject: [PATCH 5/6] Fix native library resolution for dotnet tools on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach used Path.Combine(AppContext.BaseDirectory, libraryName) which produced a path like '.../tools/any/linux-x64/libclang' without the platform-specific extension (.so/.dylib/.dll). NativeLibrary.TryLoad with an absolute path does NOT apply platform-specific name mangling, so dlopen would fail to find the file. Added platform suffix based on RuntimeInformation: Windows → .dll macOS → .dylib Linux → .so AppContext.BaseDirectory IS the correct directory for co-located native libraries in dotnet tool NuGet packages (tools/any//), confirmed by inspecting the actual package layout on disk. --- sources/ClangSharp.Interop/clang.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index d365f9a2..80bd12fc 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -30,10 +30,22 @@ private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImpo return nativeLibrary; } - // When invoked as a dotnet tool (ClangSharpPInvokeGenerator) on Unix, assemblies next to the executable aren't searched by default. - if (NativeLibrary.TryLoad(Path.Combine(AppContext.BaseDirectory, libraryName), out nativeLibrary)) + // When invoked as a dotnet tool (ClangSharpPInvokeGenerator), native libraries + // are co-located with the executable in the NuGet cache but aren't always found + // by the default resolver. Explicitly try the application base directory. + var baseDir = AppContext.BaseDirectory; + if (baseDir is not null) { - return nativeLibrary; + // NativeLibrary.TryLoad with an absolute path does not apply platform-specific + // name mangling (lib prefix, .so/.dylib/.dll suffix), so we add it explicitly. + var suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" + : ".so"; + + if (NativeLibrary.TryLoad(Path.Combine(baseDir, libraryName + suffix), out nativeLibrary)) + { + return nativeLibrary; + } } return IntPtr.Zero; From 8019f52ce7d57b54438761efff996cc96bab3631 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Sat, 4 Jul 2026 01:08:17 +0800 Subject: [PATCH 6/6] Explain in comment --- sources/ClangSharp.Interop/clang.cs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/sources/ClangSharp.Interop/clang.cs b/sources/ClangSharp.Interop/clang.cs index 80bd12fc..c36ba1f4 100644 --- a/sources/ClangSharp.Interop/clang.cs +++ b/sources/ClangSharp.Interop/clang.cs @@ -31,21 +31,18 @@ private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImpo } // When invoked as a dotnet tool (ClangSharpPInvokeGenerator), native libraries - // are co-located with the executable in the NuGet cache but aren't always found - // by the default resolver. Explicitly try the application base directory. - var baseDir = AppContext.BaseDirectory; - if (baseDir is not null) - { - // NativeLibrary.TryLoad with an absolute path does not apply platform-specific - // name mangling (lib prefix, .so/.dylib/.dll suffix), so we add it explicitly. - var suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" - : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" - : ".so"; + // are co-located with the executable in the NuGet cache but aren't found + // by the default resolver on Unix (default dlopen only searches system paths + // and LD_LIBRARY_PATH), so we explicitly try the application base directory. + // NativeLibrary.TryLoad with an absolute path does not apply platform-specific + // name mangling (lib prefix, .so/.dylib/.dll suffix), so we add it explicitly. + var suffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".dll" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" + : ".so"; - if (NativeLibrary.TryLoad(Path.Combine(baseDir, libraryName + suffix), out nativeLibrary)) - { - return nativeLibrary; - } + if (NativeLibrary.TryLoad(Path.Combine(AppContext.BaseDirectory, libraryName + suffix), out nativeLibrary)) + { + return nativeLibrary; } return IntPtr.Zero;