Standalone repo (ladybug-dotnet): a hand-written P/Invoke binding over the Ladybug C API.
Developed as a git submodule mounted at tools/csharp_api in the LadybugDB/ladybug monorepo
(like tools/rust_api / tools/java_api), so the C engine and its header (src/include/c_api/lbug.h)
live in that parent repo — reachable at ../../ when mounted, NOT in this repo. The managed
enums/structs/signatures mirror that header exactly; if they drift, calls corrupt memory or crash at
runtime, not at compile time.
Maintainer guide — read first, keep current. Durable repository operations, versioning, release,
packaging, and ABI-update guidance lives in MAINTAINING.md. Start there for non-trivial changes and
update it when workflow or release policy changes.
- Always: keep managed signatures, struct layouts, and enum values in lockstep with the upstream
lbug.h; stage a native lib and run the FULL test suite before proposing any interop change (ABI bugs never surface at compile time). - Ask first: changing marshalling, calling convention, or struct layout; bumping the pinned engine
version (the base of
version.txt); adding/dropping a RID or changingruntimes/{rid}/native/packaging. - Never: alter a struct's field order/width, the
byte-for-C-boolrule, or the who-frees-what string/blob ownership without sign-off. Never add a finalizer orSafeHandleto the disposables (deliberate — see D10). Never commit native binaries: everything underlib/is a gitignored artifact.
Managed-only — no native toolchain needed, this is what CI does (run from the repo root, i.e.
tools/csharp_api/ when mounted as the submodule):
dotnet build LadybugDB.slnx -c Release # both TFMs: net10.0 + netstandard2.0
dotnet test test/LadybugDB.Tests/LadybugDB.Tests.csproj -c ReleaseThe C# build never compiles C; it loads a prebuilt lbug_shared shared library at runtime.
Native lib + true end-to-end suite — only when checked out as the monorepo submodule (the script
builds the parent engine via ../../); Windows, MSVC + Ninja, ~8 min on first build:
pwsh -File scripts/build-native-and-test.ps1Builds lbug_shared.dll, stages it into lib/runtimes/win-x64/native/, and runs the suite. The
manual CMake recipe (other OSes, flags, the -DCMAKE_POLICY_VERSION_MINIMUM=3.5 floor) is in
MAINTAINING.md.
- Round-trip/interop tests (
SmokeTests,TypeMappingTests,PreparedStatementTests) need the native lib and skip (not fail) when it is absent. Stage a native lib first to exercise them. - ABI guard tests (
StructLayoutTests) assert every cross-boundary struct's size/offsets and run without native —dotnet test ... --filter FullyQualifiedName~StructLayoutTests. A failure here means you broke the ABI. Note: these do NOT cover enum values or function signatures. - Set
LADYBUG_REQUIRE_NATIVE=1to turn a "native didn't load" skip into a hard failure (the release CI sets it when gating on the downloaded native lib).
- Two declaration files, kept identical:
Interop/Native.LibraryImport.cs(net7+, source-generated[LibraryImport],StringMarshalling.Utf8) andInterop/Native.DllImport.cs(netstandard2.0, classic[DllImport],Cdecl, manual UTF-8 viaToUtf8). Add or change a function in both. - Cdecl; x64/arm64 only. Structs are blittable,
[StructLayout(LayoutKind.Sequential)], all inInterop/NativeTypes.cs. - C
boolis 1 byte →bytein structs,[return: MarshalAs(UnmanagedType.U1)] boolon returns. Never the default 4-byte .NETbool. **lbug_system_configis 56 bytes** — always include the macOS-only trailingthread_qosso the by-value struct matches the native ABI on every platform.- Memory ownership:
char* returns are declaredIntPtr, copied to a managed string, then freed vialbug_destroy_string(Native.TakeString); blobs vialbug_destroy_blob. The_is_owned_by_cppflag on result/tuple/value structs governs disposal — don't free what C owns. - Strings cross as NUL-terminated UTF-8 both ways.
lbug_state/lbug_data_type_idare int-backed enums; the data-type-id integer values are non-contiguous and must match the header exactly.
There is no binding generator (no ClangSharp). "Source-generated" refers only to .NET's built-in
LibraryImport marshaller. Edit interop by hand against the upstream lbug.h; don't hunt for a regen command.
The binding targets the Ladybug engine C API in the separate LadybugDB/ladybug repo, pinned to one
release: the engine tag defaults to the first three numeric segments of version.txt at the repo root
(e.g. 0.17.0.1 -> v0.17.0), overridable per build via --engine-version / ENGINE_VERSION.
The two repos are no longer a single commit: when moving to a new engine release, re-sync the managed
signatures/structs/enums against that release's src/include/c_api/lbug.h and update the ABI tests in
the same change.
The binding ships as a family of packages, not one fat package: the managed-only LadybugDB, one
LadybugDB.Native.<rid> per RID (carrying just runtimes/<rid>/native/* + an empty _._ lib marker),
and the LadybugDB.Native meta-package that depends on all of them. Consumers reference LadybugDB
plus a native package (the meta for all platforms, or a single RID for a slim app). Shipped RIDs:
win-x64, linux-x64, linux-arm64, osx-x64, osx-arm64.
A Cake Frosting build project under cake/ drives everything (don't hand-run dotnet pack):
./build.sh --target Test # build + stage host native + run the suite
./build.sh --target Pack --package-version <v> # full family into ./artifacts, then verify contentscake/BuildContext.csis the single source of truth for the RID set and the RID->(asset, library) mapping;EnsureNativeStageddownloads the pinned engine asset (gh) and extracts the canonical library intolib/runtimes/<rid>/native/(skips when one is already staged from a local source build).cake/native/LadybugDB.Native.Runtime.csprojis one template packed once per RID; the meta-package iscake/native/LadybugDB.Native.nuspec(version/commit tokens substituted at pack time)._._andSuppressDependenciesWhenPackingkeep the native packages free of compile references and framework deps.VerifyPackagesasserts the managed assemblies, every per-RID payload, and the meta's dependency set.
The release pipeline (.github/workflows/release.yml, tag v*) runs --target Test (linux-x64 gate
against the real engine) then --target Pack, and publishes all 7 packages via OIDC. The engine release
the natives come from defaults to the first three numeric segments of the package version (version.txt,
or the v* tag on release). Details + release checklist live in MAINTAINING.md.
src/include/c_api/lbug.h(in the parentLadybugDB/ladybugrepo) — source of truth for every signature, struct, and enum.MAINTAINING.md— repository operations, versioning, packaging, release, native build, and ABI update guidance.dotnet-pinvokeskill (in the monorepo's.agents/skills/) — P/Invoke technique. C# style is enforced by the .NET SDK analyzers (the build is warning-clean); don't restate it.