Skip to content

Commit

Permalink
LibNode Discovery (#425)
Browse files Browse the repository at this point in the history
Thank you for submitting this PR!
  • Loading branch information
wasabii authored Feb 18, 2025
1 parent bda132b commit e99e1d6
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 63 deletions.
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.203" />
<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
2 changes: 1 addition & 1 deletion src/NodeApi.DotNetHost/JSMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ public Expression<JSCallback> BuildFromJSConstructorExpression(ConstructorInfo c

ParameterExpression resultVariable = Expression.Variable(
constructor.DeclaringType!, "__result");
variables = new List<ParameterExpression>(argVariables.Append(resultVariable));
variables = [.. argVariables.Append(resultVariable)];
statements.Add(Expression.Assign(resultVariable,
Expression.New(constructor, argVariables)));

Expand Down
7 changes: 3 additions & 4 deletions src/NodeApi.Generator/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(
(variables ?? Enumerable.Empty<string>()).Union(
lambda.Parameters.Select((p) => p.Name!)))),
ToCS(lambda.Body, path, [.. (variables ?? Enumerable.Empty<string>()).Union(
lambda.Parameters.Select((p) => p.Name!))]),

ParameterExpression parameter =>
(parameter.IsByRef && parameter.Name?.StartsWith(OutParameterPrefix) == true) ?
Expand Down Expand Up @@ -285,7 +284,7 @@ private static string FormatStatement(
if (assignment.Left is ParameterExpression variable &&
!variables.Contains(variable.Name!))
{
variables = new HashSet<string>(variables.Union(new[] { variable.Name! }));
variables = [.. variables.Union(new[] { variable.Name! })];
s += FormatType(variable.Type) + " " + s;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ private static IEnumerable<string> MergeSystemReferenceAssemblies(

private static Version InferReferenceAssemblyVersionFromPath(string assemblyPath)
{
var pathParts = assemblyPath.Split(
List<string> pathParts = assemblyPath.Split(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).ToList();

// Infer the version from a system reference assembly path such as
Expand Down Expand Up @@ -1230,7 +1230,7 @@ private void BeginNamespace(ref SourceBuilder s, Type type)
return;
}

List<string> namespaceParts = new(type.Namespace?.Split('.') ?? Enumerable.Empty<string>());
List<string> namespaceParts = [.. type.Namespace?.Split('.') ?? Enumerable.Empty<string>()];

int namespacePartsCount = namespaceParts.Count;
Type? declaringType = type.DeclaringType;
Expand Down
217 changes: 184 additions & 33 deletions src/NodeApi/Runtime/NativeLibrary.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -39,15 +37,53 @@ 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);
#else
return SysNativeLibrary.Load(libraryName);
#endif
return LoadFromPath(libraryPath, throwOnError: true);
}

/// <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)
{
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;
}
}

/// <summary>
Expand All @@ -58,53 +94,168 @@ public static nint Load(string libraryName)
/// <returns>The address of the symbol.</returns>
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

[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;

Expand Down
Loading

0 comments on commit e99e1d6

Please sign in to comment.