Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LibNode Discovery #425

Merged
merged 18 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" /><!-- 4.1.0 is compatible with .NET Standard -->
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.JavaScript.LibNode" Version="20.1800.202" />
<PackageVersion Include="Microsoft.JavaScript.LibNode" Version="20.1800.202-alethic.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.133" />
Expand Down
7 changes: 0 additions & 7 deletions bench/Benchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ public static void Main(string[] args)
.WithOptions(ConfigOptions.JoinSummary));
}

private static string LibnodePath { get; } = Path.Combine(
GetRepoRootDirectory(),
"bin",
GetCurrentPlatformRuntimeIdentifier(),
"libnode" + GetSharedLibraryExtension());

private NodeEmbeddingRuntime? _runtime;
private NodeEmbeddingNodeApiScope? _nodeApiScope;
private JSValue _jsString;
Expand Down Expand Up @@ -89,7 +83,6 @@ public static void Method() { }
protected void Setup()
{
NodeEmbeddingPlatform platform = new(
LibnodePath,
new NodeEmbeddingPlatformSettings { Args = s_settings });

// This setup avoids using NodejsEmbeddingThreadRuntime so benchmarks can run on
Expand Down
141 changes: 130 additions & 11 deletions src/NodeApi/Runtime/NativeLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if !NET7_0_OR_GREATER

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
#if !(NETFRAMEWORK || NETSTANDARD)
using SysNativeLibrary = System.Runtime.InteropServices.NativeLibrary;
Expand Down Expand Up @@ -39,17 +40,63 @@ public static nint GetMainProgramHandle()
/// <summary>
/// Loads a native library using default flags.
/// </summary>
/// <param name="libraryName">The name of the native library to be loaded.</param>
/// <param name="libraryPath">The name of the native library to be loaded.</param>
/// <returns>The OS handle for the loaded native library.</returns>
public static nint Load(string libraryName)
public static nint Load(string libraryPath)
{
#if NETFRAMEWORK || NETSTANDARD
return LoadLibrary(libraryName);
return LoadFromPath(libraryPath, throwOnError: true);
#else
return SysNativeLibrary.Load(libraryName);
#endif
}

/// <summary>
/// Provides a simple API for loading a native library and returns a value that indicates whether the operation succeeded.
/// </summary>
/// <param name="libraryPath">The name of the native library to be loaded.</param>
/// <param name="handle">When the method returns, the OS handle of the loaded native library.</param>
/// <returns><c>true</c> if the native library was loaded successfully; otherwise, <c>false</c>.</returns>
public static bool TryLoad(string libraryPath, out nint handle)
{
#if NETFRAMEWORK || NETSTANDARD
handle = LoadFromPath(libraryPath, throwOnError: false);
return handle != 0;
#else
return SysNativeLibrary.TryLoad(libraryName);
#endif
}

static nint LoadFromPath(string libraryPath, bool throwOnError)
{
if (libraryPath is null)
throw new ArgumentNullException(nameof(libraryPath));

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
nint handle = LoadLibrary(libraryPath);
if (handle == 0 && throwOnError)
throw new DllNotFoundException(new Win32Exception(Marshal.GetLastWin32Error()).Message);

return handle;
}
else
{
dlerror();
nint handle = dlopen(libraryPath, RTLD_LAZY);
nint error = dlerror();
if (error != 0)
{
if (throwOnError)
throw new DllNotFoundException(Marshal.PtrToStringAuto(error));

handle = 0;
}

return handle;
}
}

/// <summary>
/// Gets the address of an exported symbol.
/// </summary>
Expand All @@ -59,7 +106,7 @@ public static nint Load(string libraryName)
public static nint GetExport(nint handle, string name)
{
#if NETFRAMEWORK || NETSTANDARD
return GetProcAddress(handle, name);
return GetSymbol(handle, name, throwOnError: true);
#else
return SysNativeLibrary.GetExport(handle, name);
#endif
Expand All @@ -68,25 +115,77 @@ public static nint GetExport(nint handle, string name)
public static bool TryGetExport(nint handle, string name, out nint procAddress)
{
#if NETFRAMEWORK || NETSTANDARD
procAddress = GetProcAddress(handle, name);
return procAddress != default;
procAddress = GetSymbol(handle, name, throwOnError: false);
return procAddress != 0;
#else
return SysNativeLibrary.TryGetExport(handle, name, out procAddress);
#endif
}

static nint GetSymbol(nint handle, string name, bool throwOnError)
{
if (handle == 0)
throw new ArgumentNullException(nameof(handle));
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
nint procAddress = GetProcAddress(handle, name);
if (procAddress == 0 && throwOnError)
throw new DllNotFoundException(new Win32Exception(Marshal.GetLastWin32Error()).Message);

return procAddress;
}
else
{
dlerror();
nint procAddress = dlsym(handle, name);
nint error = dlerror();
if (error != 0)
{
if (throwOnError)
throw new EntryPointNotFoundException(Marshal.PtrToStringAuto(error));

procAddress = 0;
}

return procAddress;
}
}

#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments

[DllImport("kernel32")]
private static extern nint GetModuleHandle(string? moduleName);

[DllImport("kernel32")]
[DllImport("kernel32", SetLastError = true)]
private static extern nint LoadLibrary(string moduleName);

[DllImport("kernel32")]
[DllImport("kernel32", SetLastError = true)]
private static extern nint GetProcAddress(nint hModule, string procName);

private static nint dlopen(nint fileName, int flags)
private static nint dlerror()
{
// Some Linux distros / versions have libdl version 2 only.
// Mac OS only has the unversioned library.
try
{
return dlerror2();
}
catch (DllNotFoundException)
{
return dlerror1();
}
}

[DllImport("libdl", EntryPoint = "dlerror")]
private static extern nint dlerror1();

[DllImport("libdl.so.2", EntryPoint = "dlerror")]
private static extern nint dlerror2();

private static nint dlopen(string fileName, int flags)
{
// Some Linux distros / versions have libdl version 2 only.
// Mac OS only has the unversioned library.
Expand All @@ -101,10 +200,30 @@ private static nint dlopen(nint fileName, int flags)
}

[DllImport("libdl", EntryPoint = "dlopen")]
private static extern nint dlopen1(nint fileName, int flags);
private static extern nint dlopen1(string fileName, int flags);

[DllImport("libdl.so.2", EntryPoint = "dlopen")]
private static extern nint dlopen2(nint fileName, int flags);
private static extern nint dlopen2(string fileName, int flags);

private static nint dlsym(nint handle, string name)
{
// Some Linux distros / versions have libdl version 2 only.
// Mac OS only has the unversioned library.
try
{
return dlsym2(handle, name);
}
catch (DllNotFoundException)
{
return dlsym1(handle, name);
}
}

[DllImport("libdl", EntryPoint = "dlsym")]
private static extern nint dlsym1(nint fileName, string flags);

[DllImport("libdl.so.2", EntryPoint = "dlsym")]
private static extern nint dlsym2(nint fileName, string flags);

private const int RTLD_LAZY = 1;

Expand Down
101 changes: 98 additions & 3 deletions src/NodeApi/Runtime/NodeEmbedding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
namespace Microsoft.JavaScript.NodeApi.Runtime;

using System;
using System.IO;
#if UNMANAGED_DELEGATES
using System.Runtime.CompilerServices;
#endif
using System.Runtime.InteropServices;

using static JSRuntime;
using static NodejsRuntime;

Expand All @@ -33,15 +35,108 @@ public static JSRuntime JSRuntime
}
}

public static void Initialize(string libNodePath)
#if NETFRAMEWORK || NETSTANDARD

/// <summary>
/// Discovers the fallback RID of the current platform.
/// </summary>
/// <returns></returns>
static string? GetFallbackRuntimeIdentifier()
{
string? arch = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "x86",
Architecture.X64 => "x64",
Architecture.Arm => "arm",
Architecture.Arm64 => "arm64",
_ => null,
};

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return arch is not null ? $"win-{arch}" : "win";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return arch is not null ? $"linux-{arch}" : "linux";

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return arch is not null ? $"osx-{arch}" : "osx";

return null;
}

/// <summary>
/// Returns a version of the library name with the OS specific prefix and suffix.
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
static string? MapLibraryName(string name)
{
if (name is null)
return null;

if (Path.HasExtension(name))
return name;

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return name + ".dll";

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return name + ".dylib";

return name + ".so";
}

/// <summary>
/// Scans the runtimes/{rid}/native directory relative to the application base directory for the native library.
/// </summary>
/// <returns></returns>
static string? FindLocalLibNode()
{
if (GetFallbackRuntimeIdentifier() is string rid)
if (MapLibraryName("libnode") is string fileName)
if (Path.Combine(AppContext.BaseDirectory, "runtimes", rid, "native", fileName) is string libPath)
if (File.Exists(libPath))
return libPath;

return null;
}

#endif

/// <summary>
/// Attempts to load the libnode library using the discovery logic as appropriate for the platform.
/// </summary>
/// <returns></returns>
/// <exception cref="DllNotFoundException"></exception>
static nint LoadDefaultLibNode()
{
#if NETFRAMEWORK || NETSTANDARD
// search local paths that would be provided by LibNode packages
string? path = FindLocalLibNode();
if (path is not null)
if (NativeLibrary.TryLoad(path, out nint handle))
return handle;
#else
// search using default dependency context
if (NativeLibrary.TryLoad("libnode", typeof(NodeEmbedding).Assembly, null, out nint handle))
return handle;
#endif

// attempt to load from default OS search paths
if (NativeLibrary.TryLoad("libnode", out nint defaultHandle))
return defaultHandle;

throw new DllNotFoundException("The JSRuntime cannot locate the libnode shared library.");
}

public static void Initialize(string? libNodePath)
{
if (string.IsNullOrEmpty(libNodePath)) throw new ArgumentNullException(nameof(libNodePath));
if (s_jsRuntime != null)
{
throw new InvalidOperationException(
"The JSRuntime can be initialized only once per process.");
}
nint libnodeHandle = NativeLibrary.Load(libNodePath);
nint libnodeHandle = libNodePath is null ? LoadDefaultLibNode() : NativeLibrary.Load(libNodePath);
s_jsRuntime = new NodejsRuntime(libnodeHandle);
}

Expand Down
4 changes: 2 additions & 2 deletions src/NodeApi/Runtime/NodeEmbeddingPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ public static explicit operator node_embedding_platform(NodeEmbeddingPlatform pl
/// <param name="settings">Optional platform settings.</param>
/// <exception cref="InvalidOperationException">A Node.js platform instance has already been
/// loaded in the current process.</exception>
public NodeEmbeddingPlatform(string libNodePath, NodeEmbeddingPlatformSettings? settings)
public NodeEmbeddingPlatform(NodeEmbeddingPlatformSettings? settings)
{
if (Current != null)
{
throw new InvalidOperationException(
"Only one Node.js platform instance per process is allowed.");
}
Current = this;
Initialize(libNodePath);
Initialize(settings?.LibNodePath);

using FunctorRef<node_embedding_platform_configure_callback> functorRef =
CreatePlatformConfigureFunctorRef(settings?.CreateConfigurePlatformCallback());
Expand Down
1 change: 1 addition & 0 deletions src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.JavaScript.NodeApi.Runtime;

public class NodeEmbeddingPlatformSettings
{
public string? LibNodePath { get; set; }
public NodeEmbeddingPlatformFlags? PlatformFlags { get; set; }
public string[]? Args { get; set; }
public ConfigurePlatformCallback? ConfigurePlatform { get; set; }
Expand Down
1 change: 0 additions & 1 deletion test/GCTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace Microsoft.JavaScript.NodeApi.Test;

public class GCTests
{
private static string LibnodePath { get; } = GetLibnodePath();

[Fact]
public void GCHandles()
Expand Down
Loading