Skip to content

Commit

Permalink
fix: support dotnet framework (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
sighphyre authored Feb 17, 2025
1 parent 4a2cc67 commit 4537811
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 76 deletions.
23 changes: 11 additions & 12 deletions .github/workflows/publish-dotnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,32 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
binaries=("libyggdrasilffi_x86_64-musl.so dotnet-engine/runtimes/linux-musl-x64/native libyggdrasilffi.so"
"libyggdrasilffi_arm64-musl.so dotnet-engine/runtimes/linux-musl-arm64/native libyggdrasilffi.so"
"libyggdrasilffi_arm64.so dotnet-engine/runtimes/linux-arm64/native libyggdrasilffi.so"
"libyggdrasilffi_x86_64.so dotnet-engine/runtimes/linux-x64/native libyggdrasilffi.so"
"yggdrasilffi_arm64.dll dotnet-engine/runtimes/win-arm64/native yggdrasilffi.dll"
"yggdrasilffi_x86_64.dll dotnet-engine/runtimes/win-x64/native yggdrasilffi.dll"
"yggdrasilffi_i686.dll dotnet-engine/runtimes/win-x86/native yggdrasilffi.dll"
"libyggdrasilffi_arm64.dylib dotnet-engine/runtimes/osx-arm64/native libyggdrasilffi.dylib"
"libyggdrasilffi_x86_64.dylib dotnet-engine/runtimes/osx-x64/native libyggdrasilffi.dylib")
binaries=("libyggdrasilffi_x86_64-musl.so dotnet-engine/runtimes/linux-musl-x64/native"
"libyggdrasilffi_arm64-musl.so dotnet-engine/runtimes/linux-musl-arm64/native"
"libyggdrasilffi_arm64.so dotnet-engine/runtimes/linux-arm64/native"
"libyggdrasilffi_x86_64.so dotnet-engine/runtimes/linux-x64/native"
"yggdrasilffi_arm64.dll dotnet-engine/runtimes/win-arm64/native"
"yggdrasilffi_x86_64.dll dotnet-engine/runtimes/win-x64/native"
"yggdrasilffi_i686.dll dotnet-engine/runtimes/win-x86/native"
"libyggdrasilffi_arm64.dylib dotnet-engine/runtimes/osx-arm64/native"
"libyggdrasilffi_x86_64.dylib dotnet-engine/runtimes/osx-x64/native")
for binary in "${binaries[@]}"; do
# Split each item into source (release name) and target path
src_name=$(echo "$binary" | awk '{print $1}')
target_path=$(echo "$binary" | awk '{print $2}')
binary_name=$(echo "$binary" | awk '{print $3}')
echo "Downloading $src_name to $target_path..."
mkdir -p "$target_path"
curl -L -o "$target_path/$binary_name" \
curl -L -o "$target_path/$src_name" \
-H "Authorization: token $GITHUB_TOKEN" \
"https://github.com/${{ github.repository }}/releases/download/unleash-yggdrasil-v${CORE_VERSION}/${src_name}"
echo "Downloaded from https://github.com/${{ github.repository }}/releases/download/unleash-yggdrasil-v${CORE_VERSION}/${src_name}"
if [ -f "$target_path/$binary_name" ]; then
if [ -f "$target_path/$src_name" ]; then
echo "$src_name downloaded successfully to $target_path."
else
echo "Error: $src_name could not be downloaded to $target_path."
Expand Down
87 changes: 46 additions & 41 deletions dotnet-engine/Yggdrasil.Engine/FFI.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,54 @@
using System;
using System.Runtime.InteropServices;

namespace Yggdrasil;

internal static class FFI
{
private static IntPtr _libHandle;

[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr new_engine();
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_engine(IntPtr ptr);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr get_metrics(IntPtr ptr);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr take_state(IntPtr ptr, byte[] json);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr check_enabled(IntPtr ptr, byte[] toggle_name, byte[] context, byte[] customStrategyResults);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr check_variant(IntPtr ptr, byte[] toggle_name, byte[] context, byte[] customStrategyResults);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_response(IntPtr ptr);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr count_toggle(IntPtr ptr, byte[] toggle_name, bool enabled);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr count_variant(IntPtr ptr, byte[] toggle_name, byte[] variant_name);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr should_emit_impression_event(IntPtr ptr, byte[] toggle_name);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr built_in_strategies(IntPtr ptr);
[DllImport("yggdrasilffi", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr list_known_toggles(IntPtr ptr);
static FFI()
{
_libHandle = NativeLibLoader.LoadNativeLibrary();

new_engine = Marshal.GetDelegateForFunctionPointer<NewEngineDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "new_engine"));
free_engine = Marshal.GetDelegateForFunctionPointer<FreeEngineDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "free_engine"));
get_metrics = Marshal.GetDelegateForFunctionPointer<GetMetricsDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "get_metrics"));
take_state = Marshal.GetDelegateForFunctionPointer<TakeStateDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "take_state"));
check_enabled = Marshal.GetDelegateForFunctionPointer<CheckEnabledDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "check_enabled"));
check_variant = Marshal.GetDelegateForFunctionPointer<CheckVariantDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "check_variant"));
free_response = Marshal.GetDelegateForFunctionPointer<FreeResponseDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "free_response"));
count_toggle = Marshal.GetDelegateForFunctionPointer<CountToggleDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "count_toggle"));
count_variant = Marshal.GetDelegateForFunctionPointer<CountVariantDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "count_variant"));
should_emit_impression_event = Marshal.GetDelegateForFunctionPointer<ShouldEmitImpressionEventDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "should_emit_impression_event"));
built_in_strategies = Marshal.GetDelegateForFunctionPointer<BuiltInStrategiesDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "built_in_strategies"));
list_known_toggles = Marshal.GetDelegateForFunctionPointer<ListKnownTogglesDelegate>(NativeLibLoader.LoadFunctionPointer(_libHandle, "list_known_toggles"));
}

private delegate IntPtr NewEngineDelegate();
private delegate void FreeEngineDelegate(IntPtr ptr);
private delegate IntPtr GetMetricsDelegate(IntPtr ptr);
private delegate IntPtr TakeStateDelegate(IntPtr ptr, byte[] json);
private delegate IntPtr CheckEnabledDelegate(IntPtr ptr, byte[] toggle_name, byte[] context, byte[] customStrategyResults);
private delegate IntPtr CheckVariantDelegate(IntPtr ptr, byte[] toggle_name, byte[] context, byte[] customStrategyResults);
private delegate void FreeResponseDelegate(IntPtr ptr);
private delegate IntPtr CountToggleDelegate(IntPtr ptr, byte[] toggle_name, bool enabled);
private delegate IntPtr CountVariantDelegate(IntPtr ptr, byte[] toggle_name, byte[] variant_name);
private delegate IntPtr ShouldEmitImpressionEventDelegate(IntPtr ptr, byte[] toggle_name);
private delegate IntPtr BuiltInStrategiesDelegate(IntPtr ptr);
private delegate IntPtr ListKnownTogglesDelegate(IntPtr ptr);
private static readonly NewEngineDelegate new_engine;
private static readonly FreeEngineDelegate free_engine;
private static readonly GetMetricsDelegate get_metrics;
private static readonly TakeStateDelegate take_state;
private static readonly CheckEnabledDelegate check_enabled;
private static readonly CheckVariantDelegate check_variant;
private static readonly FreeResponseDelegate free_response;
private static readonly CountToggleDelegate count_toggle;
private static readonly CountVariantDelegate count_variant;
private static readonly ShouldEmitImpressionEventDelegate should_emit_impression_event;
private static readonly BuiltInStrategiesDelegate built_in_strategies;
private static readonly ListKnownTogglesDelegate list_known_toggles;

public static IntPtr NewEngine()
{
Expand All @@ -50,22 +70,12 @@ public static IntPtr TakeState(IntPtr ptr, string json)
return take_state(ptr, ToUtf8Bytes(json));
}

public static IntPtr CheckEnabled(
IntPtr ptr,
string toggle_name,
string context,
string customStrategyResults
)
public static IntPtr CheckEnabled(IntPtr ptr, string toggle_name, string context, string customStrategyResults)
{
return check_enabled(ptr, ToUtf8Bytes(toggle_name), ToUtf8Bytes(context), ToUtf8Bytes(customStrategyResults));
}

public static IntPtr CheckVariant(
IntPtr ptr,
string toggle_name,
string context,
string customStrategyResults
)
public static IntPtr CheckVariant(IntPtr ptr, string toggle_name, string context, string customStrategyResults)
{
return check_variant(ptr, ToUtf8Bytes(toggle_name), ToUtf8Bytes(context), ToUtf8Bytes(customStrategyResults));
}
Expand Down Expand Up @@ -100,11 +110,6 @@ public static IntPtr ListKnownToggles(IntPtr ptr)
return list_known_toggles(ptr);
}

/// <summary>
/// Converts a string to a UTF-8 encoded null-terminated byte array.
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>A UTF-8 encoded null-terminated byte array.</returns>
private static byte[] ToUtf8Bytes(string input)
{
byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input);
Expand Down
179 changes: 179 additions & 0 deletions dotnet-engine/Yggdrasil.Engine/NativeLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System.Reflection;
using System.Runtime.InteropServices;

internal static class NativeLibLoader
{
internal static IntPtr LoadNativeLibrary()
{
var libName = GetBinaryName();
var tempPath = Path.Combine(Path.GetTempPath(), libName);
var assembly = Assembly.GetExecutingAssembly();
var assemblyName = assembly.GetName().Name;

if (!File.Exists(tempPath))
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{assemblyName}.{libName}"))
{
if (stream == null)
throw new FileNotFoundException($"Embedded resource {libName} not found.");

using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
stream.CopyTo(fileStream);
}
}
}

return LoadBinary(tempPath);
}

private static string GetBinaryName()
{
string os, arch, libc = "";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
os = "win";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
os = "linux";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
os = "osx";
else
throw new PlatformNotSupportedException("Unsupported OS");

if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
arch = "x86_64";
else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
arch = "arm64";
else if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
arch = IntPtr.Size == 4 ? "i686" : "x86_64";
else
throw new PlatformNotSupportedException("Unsupported CPU architecture");

if (os == "linux" && IsMusl())
libc = "-musl";

string filename = os == "win"
? $"yggdrasilffi_{arch}.dll"
: $"libyggdrasilffi_{arch}{libc}.{(os == "osx" ? "dylib" : "so")}";

return filename;
}

private static bool IsMusl()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return false;

try
{
string output = File.ReadAllText("/proc/self/maps");
return output.Contains("musl");
}
catch
{
try
{
using (var process = new System.Diagnostics.Process())
{
process.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "ldd",
Arguments = "--version",
RedirectStandardOutput = true,
UseShellExecute = false
};
process.Start();
string lddOutput = process.StandardOutput.ReadToEnd();
process.WaitForExit();

return lddOutput.Contains("musl");
}
}
catch
{
return false;
}
}
}

internal static IntPtr LoadFunctionPointer(IntPtr libHandle, string functionName)
{
Type nativeLibraryType = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices");
if (nativeLibraryType != null)
{
var getExportMethod = nativeLibraryType.GetMethod("GetExport", new[] { typeof(IntPtr), typeof(string) });
if (getExportMethod != null)
{
return (IntPtr)getExportMethod.Invoke(null, new object[] { libHandle, functionName });
}
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return GetProcAddress(libHandle, functionName);
else
return dlsym(libHandle, functionName);
}

private static IntPtr LoadBinary(string libPath)
{
IntPtr handle;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
handle = LoadWindowsLibrary(libPath);
else
handle = LoadUnixLibrary(libPath);

if (handle == IntPtr.Zero)
throw new DllNotFoundException($"Failed to load library from {libPath}");

return handle;
}

private static IntPtr LoadUnixLibrary(string libPath)
{
// Try NativeLibrary.Load (works on .NET Core 3+)
Type nativeLibraryType = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices");
if (nativeLibraryType != null)
{
var loadMethod = nativeLibraryType.GetMethod("Load", new[] { typeof(string) });
if (loadMethod != null)
return (IntPtr)loadMethod.Invoke(null, new object[] { libPath });
}
return dlopen(libPath, RTLD_NOW);
}

private static IntPtr LoadWindowsLibrary(string libPath)
{
// Try NativeLibrary.Load (works on .NET Core 3+)
Type nativeLibraryType = Type.GetType("System.Runtime.InteropServices.NativeLibrary, System.Runtime.InteropServices");
if (nativeLibraryType != null)
{
var loadMethod = nativeLibraryType.GetMethod("Load", new[] { typeof(string) });
if (loadMethod != null)
return (IntPtr)loadMethod.Invoke(null, new object[] { libPath });
}

return LoadLibrary(libPath);
}

#if NETSTANDARD
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadLibrary(string dllToLoad);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);

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

[DllImport("libdl.so.2", EntryPoint = "dlclose")]
private static extern int dlclose(IntPtr handle);

private const int RTLD_NOW = 2;

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

[DllImport("libdl.so.2", SetLastError = true, CharSet = CharSet.Ansi)]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
#endif
}
Loading

0 comments on commit 4537811

Please sign in to comment.