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)