Skip to content

Fix native library resolution for dotnet tools on Linux#700

Open
Happypig375 wants to merge 6 commits into
dotnet:mainfrom
Happypig375:fix-libclang-resolve-linux-co-located
Open

Fix native library resolution for dotnet tools on Linux#700
Happypig375 wants to merge 6 commits into
dotnet:mainfrom
Happypig375:fix-libclang-resolve-linux-co-located

Conversation

@Happypig375

@Happypig375 Happypig375 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Problem

ClangSharpPInvokeGenerator fails to load libclang and libClangSharp on Linux when run as a dotnet tool, even though the RID-specific packages (e.g. ClangSharpPInvokeGenerator.linux-x64) bundle these libraries alongside the executable.

Root causes

1. Broken TryResolveClang method: The custom DllImportResolver tried hardcoded versioned SONAMEs (libclang.so.20, libclang-20) that don't match the LLVM version ClangSharp is built against (21). This could find a wrong-version system library or fail entirely.

2. Default resolver can't find co-located libraries: The SafeDirectories search path should include 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. Both libclang.so and libClangSharp.so are co-located with the executable but never found.

Fix

1. Remove TryResolveClang entirely. The versioned SONAME approach was inherently fragile (breaks with every LLVM major version bump, different distros use different naming conventions). The method was also unnecessary — when it returned IntPtr.Zero, the default resolver should have handled it.

2. Explicitly search the assembly's own directory. After the user-provided ResolveLibrary event, try loading from Path.GetDirectoryName(assembly.Location). This handles all native dependencies (libclang, libClangSharp) uniformly on all platforms, without needing versioned SONAMEs or LD_LIBRARY_PATH workarounds.

private static IntPtr OnDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
    if (TryResolveLibrary(libraryName, assembly, searchPath, out var nativeLibrary))
        return nativeLibrary;

    var assemblyDir = Path.GetDirectoryName(assembly.Location);
    if (assemblyDir is not null && NativeLibrary.TryLoad(Path.Combine(assemblyDir, libraryName), out nativeLibrary))
        return nativeLibrary;

    return IntPtr.Zero;
}

This is a follow-up to #586 which was closed as resolved by the RID-specific tool package support (20.1.2.2+). While the RID package mechanism correctly makes native binaries available alongside the executable, the custom resolver prevented the default resolution path from finding them on Linux.

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 dotnet#586.
@Happypig375 Happypig375 force-pushed the fix-libclang-resolve-linux-co-located branch from 9dbe7ae to 7f99dd2 Compare July 3, 2026 07:51
@Happypig375 Happypig375 changed the title Fix libClang resolution on Linux when co-located in RID tool packages Fix libClang resolution on Linux: remove stale hardcoded LLVM version Jul 3, 2026
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 dotnet#586.
@Happypig375 Happypig375 changed the title Fix libClang resolution on Linux: remove stale hardcoded LLVM version Remove TryResolveClang - let default resolver handle Linux uniformly Jul 3, 2026
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.
@Happypig375 Happypig375 changed the title Remove TryResolveClang - let default resolver handle Linux uniformly Fix native library resolution for dotnet tools on Linux Jul 3, 2026
@Happypig375 Happypig375 closed this Jul 3, 2026
@Happypig375 Happypig375 reopened this Jul 3, 2026
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/<rid>/), confirmed
by inspecting the actual package layout on disk.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant