Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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, out handle);
#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