From cca80813fb6c96daa254c37b7a3a3b5bfac25d28 Mon Sep 17 00:00:00 2001 From: m96-chan Date: Wed, 4 Mar 2026 20:40:23 +0900 Subject: [PATCH] Add C# / .NET bindings for oxbitnet (Issue #7) P/Invoke wrapper over oxbitnet-ffi targeting netstandard2.1 for Unity 2021.2+ compatibility. Includes thread-safe BitNet facade, NuGet packaging with platform-specific natives, ChatConsole example, and CI publish pipeline. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/publish.yml | 81 ++++++ .../rust/crates/oxbitnet-csharp/OxBitNet.sln | 25 ++ .../rust/crates/oxbitnet-csharp/README.md | 122 ++++++++ .../examples/ChatConsole/ChatConsole.csproj | 15 + .../examples/ChatConsole/Program.cs | 55 ++++ .../oxbitnet-csharp/src/OxBitNet/BitNet.cs | 268 ++++++++++++++++++ .../src/OxBitNet/ChatMessage.cs | 21 ++ .../src/OxBitNet/GenerateOptions.cs | 23 ++ .../src/OxBitNet/LoadOptions.cs | 16 ++ .../src/OxBitNet/LoadProgress.cs | 31 ++ .../src/OxBitNet/NativeLoader.cs | 77 +++++ .../src/OxBitNet/NativeMethods.cs | 107 +++++++ .../src/OxBitNet/OxBitNet.csproj | 35 +++ .../src/OxBitNet/OxBitNetException.cs | 12 + 14 files changed, 888 insertions(+) create mode 100644 packages/rust/crates/oxbitnet-csharp/OxBitNet.sln create mode 100644 packages/rust/crates/oxbitnet-csharp/README.md create mode 100644 packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/ChatConsole.csproj create mode 100644 packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/Program.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/BitNet.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/ChatMessage.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/GenerateOptions.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadOptions.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadProgress.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeLoader.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeMethods.cs create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj create mode 100644 packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNetException.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0d09281..e2d451a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,6 +37,8 @@ jobs: "$(grep -m1 '^version' packages/rust/crates/oxbitnet-python/pyproject.toml | sed 's/.*"\(.*\)"/\1/')" check packages/rust/crates/oxbitnet-java/java/build.gradle.kts \ "$(grep -m1 '^version' packages/rust/crates/oxbitnet-java/java/build.gradle.kts | sed 's/.*"\(.*\)"/\1/')" + check packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj \ + "$(grep '' packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj | sed 's/.*\(.*\)<\/Version>/\1/' | tr -d ' ')" exit $ERRORS # ── crates.io ── @@ -307,3 +309,82 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_PASSPHRASE }} + + # ── NuGet (.NET / C#) ── + build-nuget-natives: + needs: version-check + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + rid: linux-x64 + lib: liboxbitnet_ffi.so + - os: macos-latest + target: aarch64-apple-darwin + rid: osx-arm64 + lib: liboxbitnet_ffi.dylib + - os: macos-latest + target: x86_64-apple-darwin + rid: osx-x64 + lib: liboxbitnet_ffi.dylib + - os: windows-latest + target: x86_64-pc-windows-msvc + rid: win-x64 + lib: oxbitnet_ffi.dll + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/rust + + - name: Build native library + working-directory: packages/rust + run: cargo build -p oxbitnet-ffi --release --target ${{ matrix.target }} + + - uses: actions/upload-artifact@v4 + with: + name: nuget-native-${{ matrix.rid }} + path: packages/rust/target/${{ matrix.target }}/release/${{ matrix.lib }} + + publish-nuget: + needs: build-nuget-natives + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Download native libraries + uses: actions/download-artifact@v4 + with: + pattern: nuget-native-* + path: native-tmp + + - name: Bundle natives into runtimes + run: | + CSHARP_DIR=packages/rust/crates/oxbitnet-csharp + for dir in native-tmp/nuget-native-*; do + RID=$(basename "$dir" | sed 's/nuget-native-//') + DEST="$CSHARP_DIR/runtimes/$RID/native" + mkdir -p "$DEST" + cp "$dir"/* "$DEST/" + done + + - name: Pack and publish + working-directory: packages/rust/crates/oxbitnet-csharp/src/OxBitNet + run: | + dotnet pack -c Release -o ../../nupkg + dotnet nuget push ../../nupkg/*.nupkg \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate diff --git a/packages/rust/crates/oxbitnet-csharp/OxBitNet.sln b/packages/rust/crates/oxbitnet-csharp/OxBitNet.sln new file mode 100644 index 0000000..9de2d58 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/OxBitNet.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.0.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OxBitNet", "src\OxBitNet\OxBitNet.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatConsole", "examples\ChatConsole\ChatConsole.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/packages/rust/crates/oxbitnet-csharp/README.md b/packages/rust/crates/oxbitnet-csharp/README.md new file mode 100644 index 0000000..1314384 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/README.md @@ -0,0 +1,122 @@ +# oxbitnet-csharp + +C# / .NET bindings for [oxbitnet](https://crates.io/crates/oxbitnet) — run [BitNet b1.58](https://github.com/microsoft/BitNet) ternary LLMs with GPU acceleration (wgpu). + +Part of [0xBitNet](https://github.com/m96-chan/0xBitNet). + +## Build + +First, build the native library: + +```bash +cargo build -p oxbitnet-ffi --release +``` + +Produces `target/release/liboxbitnet_ffi.so` (Linux) / `.dylib` (macOS) / `oxbitnet_ffi.dll` (Windows). + +Then build the C# project: + +```bash +cd packages/rust/crates/oxbitnet-csharp +dotnet build +``` + +## Quick Start + +```csharp +using OxBitNet; + +// Load a model +using var model = BitNet.LoadSync("model.gguf"); + +// Raw prompt +model.Generate("Hello!", token => Console.Write(token)); + +// Chat messages +model.Chat(new[] { + ChatMessage.User("Hello!") +}, token => Console.Write(token), new GenerateOptions { Temperature = 0.7f }); +``` + +## API + +### Loading + +```csharp +// Sync (blocks calling thread) +using var model = BitNet.LoadSync("model.gguf"); + +// Async +using var model = await BitNet.Load("model.gguf"); + +// With progress +using var model = BitNet.LoadSync("model.gguf", new LoadOptions { + OnProgress = p => Console.WriteLine($"[{p.Phase}] {p.Fraction * 100:F1}%") +}); +``` + +### Generation + +```csharp +// Raw prompt — tokens delivered via callback +model.Generate("Once upon a time", token => Console.Write(token)); + +// With options +model.Generate("Hello!", token => Console.Write(token), new GenerateOptions { + MaxTokens = 512, + Temperature = 0.7f, + TopK = 40, +}); + +// Async variant +await model.GenerateAsync("Hello!", token => Console.Write(token)); +``` + +### Chat + +```csharp +var messages = new[] { + ChatMessage.System("You are a helpful assistant."), + ChatMessage.User("What is 2+2?"), +}; + +model.Chat(messages, token => Console.Write(token)); + +// Async variant +await model.ChatAsync(messages, token => Console.Write(token)); +``` + +### Cleanup + +`BitNet` implements `IDisposable`. Use `using` statements or call `Dispose()` explicitly: + +```csharp +model.Dispose(); +``` + +## Generation Options + +| Field | Default | Description | +|-------|---------|-------------| +| `MaxTokens` | 256 | Maximum tokens to generate | +| `Temperature` | 1.0 | Sampling temperature | +| `TopK` | 50 | Top-k sampling | +| `RepeatPenalty` | 1.1 | Repetition penalty | +| `RepeatLastN` | 64 | Window for repetition penalty | + +## Unity + +OxBitNet targets `netstandard2.1` for Unity 2021.2+ compatibility. Place the native library in your Unity project's `Plugins` folder — Unity's plugin system handles native loading automatically. + +## Running the Example + +```bash +cd packages/rust +cargo build -p oxbitnet-ffi --release +cd crates/oxbitnet-csharp +dotnet run --project examples/ChatConsole -- /path/to/model.gguf +``` + +## License + +MIT diff --git a/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/ChatConsole.csproj b/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/ChatConsole.csproj new file mode 100644 index 0000000..808f3ff --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/ChatConsole.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + disable + 9.0 + ChatConsole + + + + + + + diff --git a/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/Program.cs b/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/Program.cs new file mode 100644 index 0000000..0b453d7 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/examples/ChatConsole/Program.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using OxBitNet; + +class Program +{ + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.Error.WriteLine("Usage: ChatConsole "); + Environment.Exit(1); + } + + string modelPath = args[0]; + + Console.Error.WriteLine($"Loading {modelPath}..."); + using var model = BitNet.LoadSync(modelPath, new LoadOptions + { + OnProgress = p => + { + Console.Error.WriteLine($" [{p.Phase}] {p.Fraction * 100:F1}%"); + } + }); + Console.Error.WriteLine("Model loaded."); + + var history = new List(); + Console.WriteLine("Type a message (or 'quit' to exit):"); + + while (true) + { + Console.Write("\n> "); + string input = Console.ReadLine(); + if (input == null || input.Trim().Equals("quit", StringComparison.OrdinalIgnoreCase)) + break; + + if (string.IsNullOrWhiteSpace(input)) + continue; + + history.Add(ChatMessage.User(input)); + + Console.Write("\n"); + var response = new System.Text.StringBuilder(); + + model.Chat(history.ToArray(), token => + { + Console.Write(token); + response.Append(token); + }, new GenerateOptions { Temperature = 0.7f }); + + Console.WriteLine(); + history.Add(ChatMessage.Assistant(response.ToString())); + } + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/BitNet.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/BitNet.cs new file mode 100644 index 0000000..06ca10e --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/BitNet.cs @@ -0,0 +1,268 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace OxBitNet +{ + /// + /// A loaded BitNet model. Thread-safe and disposable. + /// + public sealed class BitNet : IDisposable + { + private IntPtr _handle; + private readonly object _lock = new object(); + private bool _disposed; + + static BitNet() + { + NativeLoader.EnsureRegistered(); + } + + private BitNet(IntPtr handle) + { + _handle = handle; + } + + /// + /// Load a model synchronously (blocks the calling thread). + /// + public static BitNet LoadSync(string source, LoadOptions options = null) + { + byte[] sourceBytes = Encoding.UTF8.GetBytes(source + '\0'); + var sourcePin = GCHandle.Alloc(sourceBytes, GCHandleType.Pinned); + + try + { + var cOpts = NativeMethods.oxbitnet_default_load_options(); + GCHandle progressPin = default; + GCHandle cacheDirPin = default; + OxBitNetProgressFn progressDelegate = null; + + try + { + if (options?.OnProgress != null) + { + progressDelegate = (ref OxBitNetLoadProgress progress, IntPtr userdata) => + { + var p = new LoadProgress( + (LoadPhase)progress.phase, + progress.loaded, + progress.total, + progress.fraction); + options.OnProgress(p); + }; + progressPin = GCHandle.Alloc(progressDelegate); + cOpts.on_progress = Marshal.GetFunctionPointerForDelegate(progressDelegate); + } + + if (options?.CacheDir != null) + { + byte[] cacheDirBytes = Encoding.UTF8.GetBytes(options.CacheDir + '\0'); + cacheDirPin = GCHandle.Alloc(cacheDirBytes, GCHandleType.Pinned); + cOpts.cache_dir = cacheDirPin.AddrOfPinnedObject(); + } + + IntPtr handle = NativeMethods.oxbitnet_load(sourcePin.AddrOfPinnedObject(), ref cOpts); + if (handle == IntPtr.Zero) + { + throw new OxBitNetException(GetErrorMessage() ?? "Failed to load model"); + } + + return new BitNet(handle); + } + finally + { + if (progressPin.IsAllocated) progressPin.Free(); + if (cacheDirPin.IsAllocated) cacheDirPin.Free(); + } + } + finally + { + sourcePin.Free(); + } + } + + /// + /// Load a model asynchronously. + /// + public static Task Load(string source, LoadOptions options = null) + { + return Task.Run(() => LoadSync(source, options)); + } + + /// + /// Generate text from a raw prompt string. Tokens are delivered via callback. + /// + public void Generate(string prompt, Action onToken, GenerateOptions options = null) + { + IntPtr handle = AcquireHandle(); + var cOpts = MakeGenerateOptions(options); + + byte[] promptBytes = Encoding.UTF8.GetBytes(prompt + '\0'); + var promptPin = GCHandle.Alloc(promptBytes, GCHandleType.Pinned); + + OxBitNetTokenFn tokenDelegate = (IntPtr token, nuint len, IntPtr userdata) => + { + string str = Marshal.PtrToStringUTF8(token, (int)len); + onToken(str); + return 0; + }; + var tokenPin = GCHandle.Alloc(tokenDelegate); + + try + { + int ret = NativeMethods.oxbitnet_generate( + handle, + promptPin.AddrOfPinnedObject(), + ref cOpts, + Marshal.GetFunctionPointerForDelegate(tokenDelegate), + IntPtr.Zero); + + if (ret != 0) + { + throw new OxBitNetException(GetErrorMessage() ?? "Generate failed"); + } + } + finally + { + promptPin.Free(); + tokenPin.Free(); + } + } + + /// + /// Generate text from a raw prompt string asynchronously. + /// + public Task GenerateAsync(string prompt, Action onToken, GenerateOptions options = null) + { + return Task.Run(() => Generate(prompt, onToken, options)); + } + + /// + /// Generate text from chat messages. Tokens are delivered via callback. + /// + public void Chat(ChatMessage[] messages, Action onToken, GenerateOptions options = null) + { + IntPtr handle = AcquireHandle(); + var cOpts = MakeGenerateOptions(options); + + // Marshal chat messages + var cMessages = new OxBitNetChatMessage[messages.Length]; + var pins = new GCHandle[messages.Length * 2]; + int pinIdx = 0; + + try + { + for (int i = 0; i < messages.Length; i++) + { + byte[] roleBytes = Encoding.UTF8.GetBytes(messages[i].Role + '\0'); + byte[] contentBytes = Encoding.UTF8.GetBytes(messages[i].Content + '\0'); + + var rolePin = GCHandle.Alloc(roleBytes, GCHandleType.Pinned); + var contentPin = GCHandle.Alloc(contentBytes, GCHandleType.Pinned); + pins[pinIdx++] = rolePin; + pins[pinIdx++] = contentPin; + + cMessages[i].role = rolePin.AddrOfPinnedObject(); + cMessages[i].content = contentPin.AddrOfPinnedObject(); + } + + var messagesPin = GCHandle.Alloc(cMessages, GCHandleType.Pinned); + + OxBitNetTokenFn tokenDelegate = (IntPtr token, nuint len, IntPtr userdata) => + { + string str = Marshal.PtrToStringUTF8(token, (int)len); + onToken(str); + return 0; + }; + var tokenPin = GCHandle.Alloc(tokenDelegate); + + try + { + int ret = NativeMethods.oxbitnet_chat( + handle, + messagesPin.AddrOfPinnedObject(), + (nuint)messages.Length, + ref cOpts, + Marshal.GetFunctionPointerForDelegate(tokenDelegate), + IntPtr.Zero); + + if (ret != 0) + { + throw new OxBitNetException(GetErrorMessage() ?? "Chat failed"); + } + } + finally + { + messagesPin.Free(); + tokenPin.Free(); + } + } + finally + { + for (int i = 0; i < pinIdx; i++) + { + if (pins[i].IsAllocated) pins[i].Free(); + } + } + } + + /// + /// Generate text from chat messages asynchronously. + /// + public Task ChatAsync(ChatMessage[] messages, Action onToken, GenerateOptions options = null) + { + return Task.Run(() => Chat(messages, onToken, options)); + } + + /// + /// Release all GPU resources. Safe to call multiple times. + /// Also called automatically by Dispose(). + /// + public void Dispose() + { + lock (_lock) + { + if (!_disposed && _handle != IntPtr.Zero) + { + NativeMethods.oxbitnet_free(_handle); + _handle = IntPtr.Zero; + _disposed = true; + } + } + } + + private IntPtr AcquireHandle() + { + lock (_lock) + { + if (_disposed || _handle == IntPtr.Zero) + throw new ObjectDisposedException(nameof(BitNet)); + return _handle; + } + } + + private static OxBitNetGenerateOptions MakeGenerateOptions(GenerateOptions options) + { + if (options == null) + return NativeMethods.oxbitnet_default_generate_options(); + + return new OxBitNetGenerateOptions + { + max_tokens = (nuint)options.MaxTokens, + temperature = options.Temperature, + top_k = (nuint)options.TopK, + repeat_penalty = options.RepeatPenalty, + repeat_last_n = (nuint)options.RepeatLastN, + }; + } + + private static string GetErrorMessage() + { + IntPtr ptr = NativeMethods.oxbitnet_error_message(); + if (ptr == IntPtr.Zero) return null; + return Marshal.PtrToStringUTF8(ptr); + } + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/ChatMessage.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/ChatMessage.cs new file mode 100644 index 0000000..a386c82 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/ChatMessage.cs @@ -0,0 +1,21 @@ +namespace OxBitNet +{ + /// + /// A chat message with a role and content. + /// + public sealed class ChatMessage + { + public string Role { get; } + public string Content { get; } + + public ChatMessage(string role, string content) + { + Role = role; + Content = content; + } + + public static ChatMessage System(string content) => new ChatMessage("system", content); + public static ChatMessage User(string content) => new ChatMessage("user", content); + public static ChatMessage Assistant(string content) => new ChatMessage("assistant", content); + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/GenerateOptions.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/GenerateOptions.cs new file mode 100644 index 0000000..5c3565c --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/GenerateOptions.cs @@ -0,0 +1,23 @@ +namespace OxBitNet +{ + /// + /// Options for text generation. + /// + public sealed class GenerateOptions + { + /// Maximum number of tokens to generate. + public int MaxTokens { get; set; } = 256; + + /// Sampling temperature. + public float Temperature { get; set; } = 1.0f; + + /// Top-k sampling parameter. + public int TopK { get; set; } = 50; + + /// Repetition penalty. + public float RepeatPenalty { get; set; } = 1.1f; + + /// Window size for repetition penalty. + public int RepeatLastN { get; set; } = 64; + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadOptions.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadOptions.cs new file mode 100644 index 0000000..01b3a9a --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadOptions.cs @@ -0,0 +1,16 @@ +using System; + +namespace OxBitNet +{ + /// + /// Options for loading a model. + /// + public sealed class LoadOptions + { + /// Progress callback invoked during model loading. + public Action OnProgress { get; set; } + + /// Cache directory path (optional). + public string CacheDir { get; set; } + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadProgress.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadProgress.cs new file mode 100644 index 0000000..0cb3b02 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/LoadProgress.cs @@ -0,0 +1,31 @@ +namespace OxBitNet +{ + /// + /// Phase of the model loading process. + /// + public enum LoadPhase + { + Download = 0, + Parse = 1, + Upload = 2, + } + + /// + /// Progress information during model loading. + /// + public sealed class LoadProgress + { + public LoadPhase Phase { get; } + public ulong Loaded { get; } + public ulong Total { get; } + public double Fraction { get; } + + internal LoadProgress(LoadPhase phase, ulong loaded, ulong total, double fraction) + { + Phase = phase; + Loaded = loaded; + Total = total; + Fraction = fraction; + } + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeLoader.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeLoader.cs new file mode 100644 index 0000000..9938f2e --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeLoader.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace OxBitNet +{ + internal static class NativeLoader + { + private static bool _registered; + + internal static void EnsureRegistered() + { + if (_registered) return; + _registered = true; + + NativeLibrary.SetDllImportResolver(typeof(NativeLoader).Assembly, Resolver); + } + + private static IntPtr Resolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != "oxbitnet_ffi") + return IntPtr.Zero; + + string rid = GetRuntimeIdentifier(); + string fileName = GetNativeFileName(); + + // Try: runtimes/{rid}/native/{lib} (NuGet layout) + string assemblyDir = Path.GetDirectoryName(assembly.Location) ?? "."; + string nugetPath = Path.Combine(assemblyDir, "runtimes", rid, "native", fileName); + if (NativeLibrary.TryLoad(nugetPath, out IntPtr handle)) + return handle; + + // Try: same directory as assembly + string localPath = Path.Combine(assemblyDir, fileName); + if (NativeLibrary.TryLoad(localPath, out handle)) + return handle; + + // Fallback: system search path + if (NativeLibrary.TryLoad(libraryName, assembly, searchPath, out handle)) + return handle; + + return IntPtr.Zero; + } + + private static string GetRuntimeIdentifier() + { + string os; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + os = "linux"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + os = "osx"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + os = "win"; + else + os = "unknown"; + + string arch = RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => "x64", + }; + + return $"{os}-{arch}"; + } + + private static string GetNativeFileName() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return "oxbitnet_ffi.dll"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return "liboxbitnet_ffi.dylib"; + return "liboxbitnet_ffi.so"; + } + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeMethods.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeMethods.cs new file mode 100644 index 0000000..f598cb1 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/NativeMethods.cs @@ -0,0 +1,107 @@ +using System; +using System.Runtime.InteropServices; + +namespace OxBitNet +{ + internal enum OxBitNetLoadPhase : int + { + Download = 0, + Parse = 1, + Upload = 2, + } + + internal enum OxBitNetLogLevel : byte + { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct OxBitNetGenerateOptions + { + public nuint max_tokens; + public float temperature; + public nuint top_k; + public float repeat_penalty; + public nuint repeat_last_n; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct OxBitNetLoadProgress + { + public OxBitNetLoadPhase phase; + public ulong loaded; + public ulong total; + public double fraction; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void OxBitNetProgressFn(ref OxBitNetLoadProgress progress, IntPtr userdata); + + [StructLayout(LayoutKind.Sequential)] + internal struct OxBitNetLoadOptions + { + public IntPtr on_progress; // OxBitNetProgressFn + public IntPtr progress_userdata; + public IntPtr cache_dir; // const char* + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int OxBitNetTokenFn(IntPtr token, nuint len, IntPtr userdata); + + [StructLayout(LayoutKind.Sequential)] + internal struct OxBitNetChatMessage + { + public IntPtr role; // const char* + public IntPtr content; // const char* + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void OxBitNetLogFn(OxBitNetLogLevel level, IntPtr message, nuint len, IntPtr userdata); + + internal static class NativeMethods + { + private const string LibName = "oxbitnet_ffi"; + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void oxbitnet_set_logger(IntPtr callback, IntPtr userdata, byte min_level); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern OxBitNetGenerateOptions oxbitnet_default_generate_options(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern OxBitNetLoadOptions oxbitnet_default_load_options(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr oxbitnet_load(IntPtr source, ref OxBitNetLoadOptions options); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr oxbitnet_load(IntPtr source, IntPtr options); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern void oxbitnet_free(IntPtr model); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern int oxbitnet_generate( + IntPtr model, + IntPtr prompt, + ref OxBitNetGenerateOptions options, + IntPtr callback, + IntPtr userdata); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern int oxbitnet_chat( + IntPtr model, + IntPtr messages, + nuint num_messages, + ref OxBitNetGenerateOptions options, + IntPtr callback, + IntPtr userdata); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr oxbitnet_error_message(); + } +} diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj new file mode 100644 index 0000000..df3cb32 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNet.csproj @@ -0,0 +1,35 @@ + + + + netstandard2.1 + 9.0 + disable + OxBitNet + OxBitNet + + + OxBitNet + 0.5.2 + m96-chan + C# bindings for oxbitnet — run BitNet b1.58 ternary LLMs with GPU acceleration (wgpu). + https://github.com/m96-chan/0xBitNet + https://github.com/m96-chan/0xBitNet + git + MIT + bitnet;llm;inference;wgpu;ai + README.md + + + false + + + + + + + + + + + + diff --git a/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNetException.cs b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNetException.cs new file mode 100644 index 0000000..f2d3664 --- /dev/null +++ b/packages/rust/crates/oxbitnet-csharp/src/OxBitNet/OxBitNetException.cs @@ -0,0 +1,12 @@ +using System; + +namespace OxBitNet +{ + /// + /// Exception thrown by OxBitNet operations. + /// + public class OxBitNetException : Exception + { + public OxBitNetException(string message) : base(message) { } + } +}