File: SdkRootLocator.cs
Web Access
Project: src\sdk\src\Cli\dotnet-aot\dotnet-aot.csproj (dotnet-aot)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if TARGET_WINDOWS
using Microsoft.DotNet.Cli.Utils;
using Windows.Win32;
using Windows.Win32.Foundation;
#endif

namespace Microsoft.DotNet.Cli;

/// <summary>
///  Resolves the versioned SDK directory for the Native AOT bridge. Prefers the SDK directory the
///  host (the muxer, or the <c>dn</c> harness) passed in, and falls back to self-locating the
///  directory of the running <c>dotnet-aot</c> native library on disk. See
///  <c>src/Cli/dotnet-aot/SdkRootResolution.md</c>.
/// </summary>
internal static unsafe partial class SdkRootLocator
{
    /// <summary>
    ///  Resolves the versioned SDK directory. When the host supplies a non-empty
    ///  <paramref name="sdkDirArgument"/> it is authoritative (and, in debug builds, is asserted to
    ///  match where <c>dotnet-aot</c> was actually loaded from). Otherwise the directory is
    ///  self-located from the native library's own module path.
    /// </summary>
    /// <returns>The resolved SDK directory, or an empty string when it cannot be determined.</returns>
    internal static string Resolve(string sdkDirArgument)
    {
        if (!string.IsNullOrEmpty(sdkDirArgument))
        {
            AssertMatchesSelfLocation(sdkDirArgument);
            return sdkDirArgument;
        }

        return TrySelfLocateSdkDirectory() ?? string.Empty;
    }

    /// <summary>
    ///  Returns the directory of the running <c>dotnet-aot</c> native library, or
    ///  <see langword="null"/> when it cannot be determined - for example when this code runs
    ///  JIT-compiled in unit tests rather than as part of the NativeAOT image.
    /// </summary>
    internal static string? TrySelfLocateSdkDirectory()
    {
        // Only meaningful in a NativeAOT image. Under the JIT the module that owns this code is the
        // test/runtime host, not dotnet-aot.dll, so a self-location would be misleading.
        if (RuntimeFeature.IsDynamicCodeSupported)
        {
            return null;
        }

        try
        {
            string? modulePath = OperatingSystem.IsWindows()
                ? GetSelfModulePathWindows()
                : GetSelfModulePathPosix();

            return string.IsNullOrEmpty(modulePath) ? null : Path.GetDirectoryName(modulePath);
        }
        catch
        {
            // The self-locate is a best-effort fallback; it must never fault the CLI.
            return null;
        }
    }

    [Conditional("DEBUG")]
    private static void AssertMatchesSelfLocation(string sdkDirArgument)
    {
        string? selfLocated = TrySelfLocateSdkDirectory();
        Debug.Assert(
            selfLocated is null || PathsEqual(sdkDirArgument, selfLocated),
            $"Host-provided sdk_dir '{sdkDirArgument}' does not match the directory dotnet-aot was loaded from ('{selfLocated}'). " +
            "The host must pass the versioned SDK directory that contains the dotnet-aot binary.");
    }

    // The address of a method in this assembly identifies the native module that contains it.
    // [UnmanagedCallersOnly] gives the method a stable native entry point in the module to hand to
    // GetModuleHandleEx; it is never called, only its address is taken.
    [UnmanagedCallersOnly]
    private static void SelfAnchor() { }

    private static string? GetSelfModulePathWindows()
    {
#if TARGET_WINDOWS
        nint address = (nint)(delegate* unmanaged<void>)&SelfAnchor;
        HMODULE module;
        if (!PInvoke.GetModuleHandleEx(
                PInvoke.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | PInvoke.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                new PCWSTR((char*)address),
                &module))
        {
            // GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS resolves the module from an address in it;
            // GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT means we hold no reference (do not free it).
            return null;
        }

        // GetModuleFileName truncates (returning the buffer length and setting ERROR_INSUFFICIENT_BUFFER)
        // when the path does not fit, so grow the buffer and retry. Stack-allocate MAX_PATH for the common
        // case; a deliberately tiny buffer in debug builds forces the grow path so the AOT tests exercise it.
#if DEBUG
        using BufferScope<char> buffer = new(stackalloc char[8]);
#else
        using BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);
#endif
        while (true)
        {
            uint length = PInvoke.GetModuleFileName(module, buffer.AsSpan());
            if (length == 0)
            {
                return null;
            }

            if (length < (uint)buffer.Length)
            {
                return buffer.Slice(0, (int)length).ToString();
            }

            // The path did not fit (length == buffer capacity); grow and retry.
            buffer.EnsureCapacity(buffer.Length * 2);
        }
#else
        // CsWin32 (hence the Windows self-locate) is unavailable off Windows; callers fall back to
        // the host-provided sdk_dir.
        return null;
#endif
    }

    private static string? GetSelfModulePathPosix()
    {
        // Best-effort on Unix: dladdr resolves the shared object containing the given address. It is
        // mapped to the platform C runtime (libSystem/libdl/libc) by ResolveDlImport; if none load, the
        // catch in TrySelfLocateSdkDirectory falls back to the host-provided sdk_dir.
        nint address = (nint)(delegate* unmanaged<void>)&SelfAnchor;
        if (dladdr(address, out DlInfo info) == 0 || info.dli_fname == 0)
        {
            return null;
        }

        return Marshal.PtrToStringUTF8(info.dli_fname);
    }

    private static bool PathsEqual(string left, string right)
    {
        static string Normalize(string path) => Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));

        StringComparison comparison = OperatingSystem.IsWindows()
            ? StringComparison.OrdinalIgnoreCase
            : StringComparison.Ordinal;

        return string.Equals(Normalize(left), Normalize(right), comparison);
    }

    // dladdr does not live in the same library across Unix flavors - libSystem on macOS, libdl on
    // glibc, libc on musl and glibc 2.34+ - so it is imported under a sentinel name that a
    // DllImportResolver (registered in the static constructor) maps to the first library that loads.
    private const string DlLibrary = "dotnet-aot-dl";

    static SdkRootLocator()
    {
        if (!OperatingSystem.IsWindows())
        {
            NativeLibrary.SetDllImportResolver(typeof(SdkRootLocator).Assembly, ResolveDlImport);
        }
    }

    private static nint ResolveDlImport(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath)
    {
        if (libraryName != DlLibrary)
        {
            return 0;
        }

        string[] candidates = OperatingSystem.IsMacOS()
            ? ["libSystem.dylib"]
            : ["libdl.so.2", "libc.so.6", "libc.so"];

        foreach (string candidate in candidates)
        {
            if (NativeLibrary.TryLoad(candidate, assembly, searchPath, out nint handle))
            {
                return handle;
            }
        }

        return 0;
    }

    [LibraryImport(DlLibrary, EntryPoint = "dladdr")]
    private static partial int dladdr(nint addr, out DlInfo info);

    [StructLayout(LayoutKind.Sequential)]
    private struct DlInfo
    {
        public nint dli_fname;
        public nint dli_fbase;
        public nint dli_sname;
        public nint dli_saddr;
    }
}