-
Notifications
You must be signed in to change notification settings - Fork 90
feat(sdk): add .NET SDK over libmoss (closes #431) #460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Adityacrypto-del
wants to merge
3
commits into
usemoss:main
Choose a base branch
from
Adityacrypto-del:sdk-dotnet
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
de8747c
feat(sdk): add .NET SDK over libmoss (closes #431)
Adityacrypto-del 26a9bdc
fix(sdk): address review — snapshot inputs, cancellable gate, SafeHandle
Adityacrypto-del 3d068af
fix(sdk): validate required strings before marshaling; don't dispose …
Adityacrypto-del File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| ## .NET build output | ||
| bin/ | ||
| obj/ | ||
| *.user | ||
|
|
||
| ## Test / coverage artifacts | ||
| [Tt]est[Rr]esults/ | ||
| *.trx | ||
| *.coverage | ||
| coverage*.json | ||
| coverage*.xml | ||
| coverage*.cobertura.xml | ||
|
|
||
| ## NuGet | ||
| *.nupkg | ||
| *.snupkg |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||
| # Visual Studio Version 17 | ||
| VisualStudioVersion = 17.0.31903.59 | ||
| MinimumVisualStudioVersion = 10.0.40219.1 | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moss", "src\Moss\Moss.csproj", "{A1B2C3D4-0001-4000-8000-000000000001}" | ||
| EndProject | ||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Moss.Tests", "tests\Moss.Tests\Moss.Tests.csproj", "{A1B2C3D4-0002-4000-8000-000000000002}" | ||
| EndProject | ||
| Global | ||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
| Debug|Any CPU = Debug|Any CPU | ||
| Release|Any CPU = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {A1B2C3D4-0001-4000-8000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {A1B2C3D4-0001-4000-8000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {A1B2C3D4-0001-4000-8000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {A1B2C3D4-0001-4000-8000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| {A1B2C3D4-0002-4000-8000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {A1B2C3D4-0002-4000-8000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {A1B2C3D4-0002-4000-8000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
| {A1B2C3D4-0002-4000-8000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU | ||
| EndGlobalSection | ||
| GlobalSection(SolutionProperties) = preSolution | ||
| HideSolutionNode = FALSE | ||
| EndGlobalSection | ||
| EndGlobal |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # Moss .NET SDK | ||
|
|
||
| The .NET SDK for [Moss](https://github.com/usemoss/moss) — fast on-device | ||
| retrieval. It wraps the native `libmoss` runtime through P/Invoke and exposes an | ||
| idiomatic, async C# API for index management, hybrid search, and metadata | ||
| filtering. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| ┌──────────────────────────────────┐ | ||
| │ Your application code │ | ||
| └──────────────┬───────────────────┘ | ||
| │ | ||
| ┌──────────────▼───────────────────┐ | ||
| │ Moss (managed C#) │ ← src/Moss | ||
| │ MossClient — async API for │ | ||
| │ indexing, querying, management │ | ||
| └──────────────┬───────────────────┘ | ||
| │ P/Invoke ([DllImport("moss")]) | ||
| ┌──────────────▼───────────────────┐ | ||
| │ libmoss (native C ABI) │ ← prebuilt runtime | ||
| │ hybrid search, data models │ | ||
| └──────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| - `src/Moss/` — the public SDK. `MossClient` plus the data models. | ||
| - `src/Moss/Interop/` — the P/Invoke layer: raw `libmoss` declarations, C-ABI | ||
| struct mirrors, UTF-8 marshaling, and native-memory conversion/cleanup. | ||
|
|
||
| The interop layer targets the same stable C ABI (`libmoss.h`) that the Go | ||
| bindings bind via cgo. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```csharp | ||
| using Moss; | ||
|
|
||
| using var client = new MossClient("your_project_id", "your_project_key"); | ||
|
|
||
| await client.CreateIndexAsync("support-docs", new[] | ||
| { | ||
| new DocumentInfo("1", "Refunds are processed within 3-5 business days."), | ||
| new DocumentInfo("2", "You can track your order on the dashboard."), | ||
| }); | ||
|
|
||
| await client.LoadIndexAsync("support-docs"); | ||
|
|
||
| var results = await client.QueryAsync( | ||
| "support-docs", "how long do refunds take?", new QueryOptions { TopK = 3 }); | ||
|
|
||
| foreach (var doc in results.Docs) | ||
| Console.WriteLine($"[{doc.Score:F3}] {doc.Text}"); | ||
| ``` | ||
|
|
||
| ### Metadata filtering | ||
|
|
||
| Attach string metadata at index time and pass a JSON filter at query time: | ||
|
|
||
| ```csharp | ||
| await client.AddDocsAsync("support-docs", new[] | ||
| { | ||
| new DocumentInfo("3", "EU refund policy…", | ||
| metadata: new Dictionary<string, string> { ["region"] = "eu" }), | ||
| }); | ||
|
|
||
| var results = await client.QueryAsync("support-docs", "refund policy", | ||
| new QueryOptions | ||
| { | ||
| TopK = 5, | ||
| FilterJson = "{\"region\": \"eu\"}", | ||
| }); | ||
| ``` | ||
|
|
||
| ## API surface | ||
|
|
||
| | Area | Methods | | ||
| |------|---------| | ||
| | Indexes | `CreateIndexAsync`, `GetIndexAsync`, `ListIndexesAsync`, `DeleteIndexAsync` | | ||
| | Documents | `AddDocsAsync`, `DeleteDocsAsync`, `GetDocsAsync` | | ||
| | Jobs | `GetJobStatusAsync` | | ||
| | Local runtime | `LoadIndexAsync`, `UnloadIndexAsync`, `RefreshIndexAsync`, `QueryAsync` | | ||
|
|
||
| All methods are asynchronous and accept a `CancellationToken`. Failures from the | ||
| native runtime surface as `MossException` (carrying the status `Code` and the | ||
| `moss_last_error` message). | ||
|
|
||
| ## The native runtime | ||
|
|
||
| The SDK calls into `libmoss`, distributed as a prebuilt native library | ||
| (`libmoss.so` on Linux, `libmoss.dylib` on macOS, `moss.dll` on Windows). It | ||
| must be discoverable at runtime — on the standard library search path, next to | ||
| your application, or via `NativeLibrary` resolution. Building and unit-testing | ||
| the SDK does **not** require the native library; only running queries does. | ||
|
|
||
| ## Building and testing | ||
|
|
||
| Requires the [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0). | ||
|
|
||
| ```bash | ||
| cd sdks/dotnet | ||
| dotnet build | ||
| dotnet test # unit tests run without libmoss | ||
| ``` | ||
|
|
||
| The unit tests cover the managed logic and the marshaling layer (UTF-8 | ||
| round-trips, native buffer packing, ABI struct sizes) and do not load the native | ||
| library. | ||
|
|
||
| ## License | ||
|
|
||
| [BSD 2-Clause License](../../LICENSE) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Moss.Interop; | ||
|
|
||
| /// <summary> | ||
| /// Owns the native <c>MossClient*</c>. Deriving from <see cref="SafeHandle"/> | ||
| /// means the handle is reference-counted during P/Invoke calls and freed via | ||
| /// <c>moss_client_free</c> from <see cref="ReleaseHandle"/> — including during | ||
| /// finalization, so a leaked (undisposed) client still releases native state. | ||
| /// </summary> | ||
| internal sealed class MossClientHandle : SafeHandle | ||
| { | ||
| public MossClientHandle() : base(IntPtr.Zero, ownsHandle: true) { } | ||
|
|
||
| public override bool IsInvalid => handle == IntPtr.Zero; | ||
|
|
||
| /// <summary>Adopt a raw handle produced by <c>moss_client_new</c>.</summary> | ||
| internal void SetRawHandle(IntPtr raw) => SetHandle(raw); | ||
|
|
||
| protected override bool ReleaseHandle() | ||
| { | ||
| NativeMethods.moss_client_free(handle); | ||
| return true; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Moss.Interop; | ||
|
|
||
| /// <summary>UTF-8 string marshaling helpers for the native boundary.</summary> | ||
| internal static class Utf8 | ||
| { | ||
| /// <summary>Allocates a NUL-terminated UTF-8 copy of <paramref name="value"/>, or | ||
| /// <see cref="IntPtr.Zero"/> when it is null. Free with <see cref="Marshal.FreeCoTaskMem"/>.</summary> | ||
| public static IntPtr Alloc(string? value) | ||
| => value is null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(value); | ||
|
|
||
| /// <summary>Reads a NUL-terminated UTF-8 string, mapping a null pointer to "".</summary> | ||
| public static string Read(IntPtr ptr) | ||
| => ptr == IntPtr.Zero ? string.Empty : Marshal.PtrToStringUTF8(ptr) ?? string.Empty; | ||
|
|
||
| /// <summary>Reads a NUL-terminated UTF-8 string, preserving null as null (for optional fields).</summary> | ||
| public static string? ReadOptional(IntPtr ptr) | ||
| => ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Tracks native allocations made while marshaling inputs and frees them all on | ||
| /// <see cref="Dispose"/>. Strings are allocated via CoTaskMem; raw blocks via HGlobal. | ||
| /// </summary> | ||
| internal sealed class NativeArena : IDisposable | ||
| { | ||
| private readonly List<IntPtr> _coTaskMem = new(); | ||
| private readonly List<IntPtr> _hGlobal = new(); | ||
|
|
||
| /// <summary>Allocate a UTF-8 string tracked by this arena.</summary> | ||
| public IntPtr String(string? value) | ||
| { | ||
| IntPtr p = Utf8.Alloc(value); | ||
| if (p != IntPtr.Zero) _coTaskMem.Add(p); | ||
| return p; | ||
| } | ||
|
|
||
| /// <summary>Allocate a raw block of <paramref name="bytes"/> bytes tracked by this arena.</summary> | ||
| public IntPtr Alloc(int bytes) | ||
| { | ||
| IntPtr p = Marshal.AllocHGlobal(bytes); | ||
| _hGlobal.Add(p); | ||
| return p; | ||
| } | ||
|
|
||
| /// <summary>Marshal an array of contiguous structs into a tracked native block.</summary> | ||
| public IntPtr StructArray<T>(IReadOnlyList<T> items) where T : struct | ||
| { | ||
| if (items.Count == 0) return IntPtr.Zero; | ||
| int size = Marshal.SizeOf<T>(); | ||
| IntPtr block = Alloc(size * items.Count); | ||
| for (int i = 0; i < items.Count; i++) | ||
| Marshal.StructureToPtr(items[i], block + i * size, false); | ||
| return block; | ||
| } | ||
|
|
||
| /// <summary>Marshal an array of floats into a tracked native block.</summary> | ||
| public IntPtr FloatArray(IReadOnlyList<float>? values) | ||
| { | ||
| if (values is null || values.Count == 0) return IntPtr.Zero; | ||
| IntPtr block = Alloc(sizeof(float) * values.Count); | ||
| var tmp = new float[values.Count]; | ||
| for (int i = 0; i < values.Count; i++) tmp[i] = values[i]; | ||
| Marshal.Copy(tmp, 0, block, tmp.Length); | ||
| return block; | ||
| } | ||
|
|
||
| /// <summary>Marshal an array of UTF-8 strings into a tracked native array of char*.</summary> | ||
| public IntPtr StringArray(IReadOnlyList<string> values) | ||
| { | ||
| if (values.Count == 0) return IntPtr.Zero; | ||
| IntPtr block = Alloc(IntPtr.Size * values.Count); | ||
| for (int i = 0; i < values.Count; i++) | ||
| Marshal.WriteIntPtr(block, i * IntPtr.Size, String(values[i])); | ||
| return block; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| foreach (IntPtr p in _coTaskMem) Marshal.FreeCoTaskMem(p); | ||
| foreach (IntPtr p in _hGlobal) Marshal.FreeHGlobal(p); | ||
| _coTaskMem.Clear(); | ||
| _hGlobal.Clear(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CONSIDER The runtime requirement is understated.
MossClientcallsmoss_client_newin its constructor, so any valid client creation or management call requires the native library, not only queries.Fix the docs to say
libmossis required whenever an application constructs/usesMossClient, or lazy-initialize native runtime only for local query operations if management APIs are meant to work without it.