diff --git a/Directory.Packages.props b/Directory.Packages.props
index ab77dced..3fdb20c6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,7 +7,7 @@
-
+
diff --git a/bench/Benchmarks.cs b/bench/Benchmarks.cs
index 228f836d..c3461ab5 100644
--- a/bench/Benchmarks.cs
+++ b/bench/Benchmarks.cs
@@ -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;
@@ -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
diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs
index baa2122b..aa8ec644 100644
--- a/src/NodeApi.DotNetHost/JSMarshaller.cs
+++ b/src/NodeApi.DotNetHost/JSMarshaller.cs
@@ -475,7 +475,7 @@ public Expression BuildFromJSConstructorExpression(ConstructorInfo c
ParameterExpression resultVariable = Expression.Variable(
constructor.DeclaringType!, "__result");
- variables = new List(argVariables.Append(resultVariable));
+ variables = [.. argVariables.Append(resultVariable)];
statements.Add(Expression.Assign(resultVariable,
Expression.New(constructor, argVariables)));
diff --git a/src/NodeApi.Generator/ExpressionExtensions.cs b/src/NodeApi.Generator/ExpressionExtensions.cs
index 32d9aaf8..96fa26f3 100644
--- a/src/NodeApi.Generator/ExpressionExtensions.cs
+++ b/src/NodeApi.Generator/ExpressionExtensions.cs
@@ -59,9 +59,8 @@ private static string ToCS(
(variables is null ? FormatType(lambda.ReturnType) + " " + lambda.Name + "(" +
string.Join(", ", lambda.Parameters.Select((p) => p.ToCS())) + ")\n" :
"(" + string.Join(", ", lambda.Parameters.Select((p) => p.ToCS())) + ") =>\n") +
- ToCS(lambda.Body, path, new HashSet(
- (variables ?? Enumerable.Empty()).Union(
- lambda.Parameters.Select((p) => p.Name!)))),
+ ToCS(lambda.Body, path, [.. (variables ?? Enumerable.Empty()).Union(
+ lambda.Parameters.Select((p) => p.Name!))]),
ParameterExpression parameter =>
(parameter.IsByRef && parameter.Name?.StartsWith(OutParameterPrefix) == true) ?
@@ -285,7 +284,7 @@ private static string FormatStatement(
if (assignment.Left is ParameterExpression variable &&
!variables.Contains(variable.Name!))
{
- variables = new HashSet(variables.Union(new[] { variable.Name! }));
+ variables = [.. variables.Union(new[] { variable.Name! })];
s += FormatType(variable.Type) + " " + s;
}
}
diff --git a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
index f0c32532..80e4405d 100644
--- a/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
+++ b/src/NodeApi.Generator/TypeDefinitionsGenerator.cs
@@ -333,7 +333,7 @@ private static IEnumerable MergeSystemReferenceAssemblies(
private static Version InferReferenceAssemblyVersionFromPath(string assemblyPath)
{
- var pathParts = assemblyPath.Split(
+ List pathParts = assemblyPath.Split(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).ToList();
// Infer the version from a system reference assembly path such as
@@ -1230,7 +1230,7 @@ private void BeginNamespace(ref SourceBuilder s, Type type)
return;
}
- List namespaceParts = new(type.Namespace?.Split('.') ?? Enumerable.Empty());
+ List namespaceParts = [.. type.Namespace?.Split('.') ?? Enumerable.Empty()];
int namespacePartsCount = namespaceParts.Count;
Type? declaringType = type.DeclaringType;
diff --git a/src/NodeApi/Runtime/NativeLibrary.cs b/src/NodeApi/Runtime/NativeLibrary.cs
index 6ab1a4bd..2628bb4a 100644
--- a/src/NodeApi/Runtime/NativeLibrary.cs
+++ b/src/NodeApi/Runtime/NativeLibrary.cs
@@ -1,13 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-#if !NET7_0_OR_GREATER
+#if !NETCOREAPP3_0_OR_GREATER
using System;
+using System.ComponentModel;
using System.Runtime.InteropServices;
-#if !(NETFRAMEWORK || NETSTANDARD)
-using SysNativeLibrary = System.Runtime.InteropServices.NativeLibrary;
-#endif
namespace Microsoft.JavaScript.NodeApi.Runtime;
@@ -39,15 +37,53 @@ public static nint GetMainProgramHandle()
///
/// Loads a native library using default flags.
///
- /// The name of the native library to be loaded.
+ /// The name of the native library to be loaded.
/// The OS handle for the loaded native library.
- public static nint Load(string libraryName)
+ public static nint Load(string libraryPath)
{
-#if NETFRAMEWORK || NETSTANDARD
- return LoadLibrary(libraryName);
-#else
- return SysNativeLibrary.Load(libraryName);
-#endif
+ return LoadFromPath(libraryPath, throwOnError: true);
+ }
+
+ ///
+ /// Provides a simple API for loading a native library and returns a value that indicates whether the operation succeeded.
+ ///
+ /// The name of the native library to be loaded.
+ /// When the method returns, the OS handle of the loaded native library.
+ /// true if the native library was loaded successfully; otherwise, false.
+ public static bool TryLoad(string libraryPath, out nint handle)
+ {
+ handle = LoadFromPath(libraryPath, throwOnError: false);
+ return handle != 0;
+ }
+
+ 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;
+ }
}
///
@@ -58,21 +94,45 @@ public static nint Load(string libraryName)
/// The address of the symbol.
public static nint GetExport(nint handle, string name)
{
-#if NETFRAMEWORK || NETSTANDARD
- return GetProcAddress(handle, name);
-#else
- return SysNativeLibrary.GetExport(handle, name);
-#endif
+ return GetSymbol(handle, name, throwOnError: true);
}
public static bool TryGetExport(nint handle, string name, out nint procAddress)
{
-#if NETFRAMEWORK || NETSTANDARD
- procAddress = GetProcAddress(handle, name);
- return procAddress != default;
-#else
- return SysNativeLibrary.TryGetExport(handle, name, out procAddress);
-#endif
+ procAddress = GetSymbol(handle, name, throwOnError: false);
+ return procAddress != 0;
+ }
+
+ 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 EntryPointNotFoundException(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
@@ -80,31 +140,122 @@ public static bool TryGetExport(nint handle, string name, out nint procAddress)
[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 delegate nint DlErrorDelegate();
+ private static DlErrorDelegate? s_dlerror;
+
+ private static nint dlerror()
{
- // Some Linux distros / versions have libdl version 2 only.
- // Mac OS only has the unversioned library.
+ // cache dlerror function
+ if (s_dlerror is not null)
+ return s_dlerror();
+
+ // some operating systems have dlerror in libc, some in libdl, some in libdl.so.2
+ // attempt in that order
try
{
- return dlopen2(fileName, flags);
+ return dlerror0();
}
- catch (DllNotFoundException)
+ catch (EntryPointNotFoundException)
{
- return dlopen1(fileName, flags);
+ try
+ {
+ return (s_dlerror = dlerror1)();
+ }
+ catch (DllNotFoundException)
+ {
+ return (s_dlerror = dlerror2)();
+ }
}
}
- [DllImport("libdl", EntryPoint = "dlopen")]
- private static extern nint dlopen1(nint fileName, int flags);
+ [DllImport("c", EntryPoint = "dlerror")]
+ private static extern nint dlerror0();
+
+ [DllImport("dl", EntryPoint = "dlerror")]
+ private static extern nint dlerror1();
+
+ [DllImport("libdl.so.2", EntryPoint = "dlerror")]
+ private static extern nint dlerror2();
+
+ private delegate nint DlOpenDelegate(string? fileName, int flags);
+ private static DlOpenDelegate? s_dlopen;
+
+ private static nint dlopen(string? fileName, int flags)
+ {
+ // cache dlopen function
+ if (s_dlopen is not null)
+ return s_dlopen(fileName, flags);
+
+ // some operating systems have dlopen in libc, some in libdl, some in libdl.so.2
+ // attempt in that order
+ try
+ {
+ return dlopen0(fileName, flags);
+ }
+ catch (EntryPointNotFoundException)
+ {
+ try
+ {
+ return (s_dlopen = dlopen1)(fileName, flags);
+ }
+ catch (DllNotFoundException)
+ {
+ return (s_dlopen = dlopen2)(fileName, flags);
+ }
+ }
+ }
+
+ [DllImport("c", EntryPoint = "dlopen")]
+ private static extern nint dlopen0(string? fileName, int flags);
+
+ [DllImport("dl", EntryPoint = "dlopen")]
+ 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 delegate nint DlSymDelegate(nint handle, string symbol);
+ private static DlSymDelegate? s_dlsym;
+
+ private static nint dlsym(nint handle, string symbol)
+ {
+ // cache dlsym function
+ if (s_dlsym is not null)
+ return s_dlsym(handle, symbol);
+
+ // some operating systems have dlsym in libc, some in libdl, some in libdl.so.2
+ // attempt in that order
+ try
+ {
+ return dlsym0(handle, symbol);
+ }
+ catch (EntryPointNotFoundException)
+ {
+ try
+ {
+ return (s_dlsym = dlsym1)(handle, symbol);
+ }
+ catch (DllNotFoundException)
+ {
+ return (s_dlsym = dlsym2)(handle, symbol);
+ }
+ }
+ }
+
+ [DllImport("c", EntryPoint = "dlsym")]
+ private static extern nint dlsym0(nint handle, string symbol);
+
+ [DllImport("dl", EntryPoint = "dlsym")]
+ private static extern nint dlsym1(nint handle, string symbol);
+
+ [DllImport("libdl.so.2", EntryPoint = "dlsym")]
+ private static extern nint dlsym2(nint handle, string symbol);
private const int RTLD_LAZY = 1;
diff --git a/src/NodeApi/Runtime/NodeEmbedding.cs b/src/NodeApi/Runtime/NodeEmbedding.cs
index 39f63c22..7c9b74d1 100644
--- a/src/NodeApi/Runtime/NodeEmbedding.cs
+++ b/src/NodeApi/Runtime/NodeEmbedding.cs
@@ -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;
@@ -33,15 +35,108 @@ public static JSRuntime JSRuntime
}
}
- public static void Initialize(string libNodePath)
+#if NETFRAMEWORK || NETSTANDARD
+
+ ///
+ /// Discovers the fallback RID of the current platform.
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Returns a version of the library name with the OS specific prefix and suffix.
+ ///
+ ///
+ ///
+ 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";
+ }
+
+ ///
+ /// Scans the runtimes/{rid}/native directory relative to the application base directory for the native library.
+ ///
+ ///
+ 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
+
+ ///
+ /// Attempts to load the libnode library using the discovery logic as appropriate for the platform.
+ ///
+ ///
+ ///
+ 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);
}
diff --git a/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs b/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs
index ccf69f02..4cb13be0 100644
--- a/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs
+++ b/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs
@@ -29,7 +29,7 @@ public static explicit operator node_embedding_platform(NodeEmbeddingPlatform pl
/// Optional platform settings.
/// A Node.js platform instance has already been
/// loaded in the current process.
- public NodeEmbeddingPlatform(string libNodePath, NodeEmbeddingPlatformSettings? settings)
+ public NodeEmbeddingPlatform(NodeEmbeddingPlatformSettings? settings)
{
if (Current != null)
{
@@ -37,7 +37,7 @@ public NodeEmbeddingPlatform(string libNodePath, NodeEmbeddingPlatformSettings?
"Only one Node.js platform instance per process is allowed.");
}
Current = this;
- Initialize(libNodePath);
+ Initialize(settings?.LibNodePath);
using FunctorRef functorRef =
CreatePlatformConfigureFunctorRef(settings?.CreateConfigurePlatformCallback());
diff --git a/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs b/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs
index 9bf6d580..d64477ab 100644
--- a/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs
+++ b/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs
@@ -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; }
diff --git a/src/NodeApi/Runtime/TracingJSRuntime.cs b/src/NodeApi/Runtime/TracingJSRuntime.cs
index 27aa2534..e78de8df 100644
--- a/src/NodeApi/Runtime/TracingJSRuntime.cs
+++ b/src/NodeApi/Runtime/TracingJSRuntime.cs
@@ -178,7 +178,7 @@ private string Format(napi_env env, napi_value value)
valueString = $" {GetValueString(env, functionName)}()";
}
break;
- };
+ }
return $"{value.Handle:X16} {valueType.ToString().Substring(5)}{valueString}";
}
diff --git a/test/GCTests.cs b/test/GCTests.cs
index 3d1ddedc..441d21e9 100644
--- a/test/GCTests.cs
+++ b/test/GCTests.cs
@@ -10,7 +10,6 @@ namespace Microsoft.JavaScript.NodeApi.Test;
public class GCTests
{
- private static string LibnodePath { get; } = GetLibnodePath();
[Fact]
public void GCHandles()
diff --git a/test/NodejsEmbeddingTests.cs b/test/NodejsEmbeddingTests.cs
index 660ac683..b2703916 100644
--- a/test/NodejsEmbeddingTests.cs
+++ b/test/NodejsEmbeddingTests.cs
@@ -22,11 +22,9 @@ public class NodejsEmbeddingTests
private static string MainScript { get; } =
"globalThis.require = require('module').createRequire(process.execPath);\n";
- private static string LibnodePath { get; } = GetLibnodePath();
-
// The Node.js platform may only be initialized once per process.
internal static NodeEmbeddingPlatform NodejsPlatform { get; } =
- new(LibnodePath, new NodeEmbeddingPlatformSettings
+ new(new NodeEmbeddingPlatformSettings
{
Args = new[] { "node", "--expose-gc" }
});
diff --git a/test/TestUtils.cs b/test/TestUtils.cs
index 81704549..b52c09cf 100644
--- a/test/TestUtils.cs
+++ b/test/TestUtils.cs
@@ -77,11 +77,6 @@ public static string GetSharedLibraryExtension()
else return ".so";
}
- public static string GetLibnodePath() =>
- Path.Combine(
- Path.GetDirectoryName(GetAssemblyLocation()) ?? string.Empty,
- "libnode" + GetSharedLibraryExtension());
-
public static string? LogOutput(
Process process,
StreamWriter logWriter)