| 1 | # ADR-0003: Native C ABI (`libwitdb`) for pywitdb |
| 2 | |
| 3 | **Статус:** принято |
| 4 | **Дата:** 2026-06-06 |
| 5 | **Репозиторий:** [AI-Guiders/pywitdb](https://github.com/AI-Guiders/pywitdb) |
| 6 | **Реализация:** [AI-Guiders/WitDatabase](https://github.com/AI-Guiders/WitDatabase) → `Sources/Core/OutWit.Database.Native` |
| 7 | **Связано:** [ADR-0001](0001-binding-architecture-bridge-and-native.md), [ADR-0002](0002-bridge-provider-keys-and-reopen.md) |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Решение (одной фразой) |
| 12 | |
| 13 | **NativeAOT shared library** `witdb` с **cdecl** C exports (`witdb_*`), opaque handles, caller-freed buffers; pywitdb грузит через **ctypes**; reopen/provider keys — те же Tier-1/2, что ADR-0002 (логика в Core, не в Python). |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## ABI versioning |
| 18 | |
| 19 | | Константа | Значение | |
| 20 | |-----------|----------| |
| 21 | | `WITDB_ABI_VERSION` | **1** (header + `witdb_abi_version()`) | |
| 22 | |
| 23 | Minor-compatible: patch-level добавления (новые error codes в конце enum) — OK. Breaking → bump ABI version + отдельная `.so`/`.dll`. |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Соглашения |
| 28 | |
| 29 | | Параметр | Значение | |
| 30 | |----------|----------| |
| 31 | | Calling convention | **cdecl** (`CallConvCdecl`) | |
| 32 | | Strings | UTF-8, null-terminated | |
| 33 | | Binary blobs | `(const uint8_t* data, uint32_t length)` | |
| 34 | | Handles | `uintptr_t` opaque (`0` = invalid) | |
| 35 | | Thread safety | Отдельные handles — не shared across threads без внешней синхронизации | |
| 36 | | Allocations | `witdb_get` / `witdb_txn_get` → буфер через `witdb_buffer_free` | |
| 37 | |
| 38 | --- |
| 39 | |
| 40 | ## Error codes (`WitDbStatus`) |
| 41 | |
| 42 | | Code | Name | Смысл | |
| 43 | |------|------|-------| |
| 44 | | 0 | `OK` | Успех | |
| 45 | | 1 | `INVALID_ARGUMENT` | NULL pointer, zero length где запрещено | |
| 46 | | 2 | `NOT_FOUND` | Файл/store не найден (`create=false`) | |
| 47 | | 3 | `PASSWORD_REQUIRED` | Encrypted store, password не передан | |
| 48 | | 4 | `WRONG_PASSWORD` | Decrypt/auth failure | |
| 49 | | 5 | `CONFIG_MISMATCH` | `ConfigurationMismatchException` | |
| 50 | | 6 | `UNKNOWN_PROVIDER` | `ProviderNotFoundException` (ADR-0002 Tier-3) | |
| 51 | | 7 | `TXN_NOT_SUPPORTED` | Store без transactions | |
| 52 | | 8 | `TXN_ACTIVE` | Вложенная txn на том же db handle | |
| 53 | | 9 | `STORE_ERROR` | Прочая ошибка движка | |
| 54 | | 10 | `INVALID_HANDLE` | Битый handle | |
| 55 | |
| 56 | `witdb_last_error_message()` — UTF-8 detail для последней ошибки в потоке (best-effort). |
| 57 | |
| 58 | --- |
| 59 | |
| 60 | ## API surface (v1) |
| 61 | |
| 62 | ```c |
| 63 | // include/witdb.h |
| 64 | uint32_t witdb_abi_version(void); |
| 65 | |
| 66 | WitDbStatus witdb_open( |
| 67 | const char* path, |
| 68 | const char* password, // NULL = none |
| 69 | uint8_t create_if_missing, // 1 = CreateOrOpen semantics |
| 70 | uintptr_t* out_db); |
| 71 | |
| 72 | WitDbStatus witdb_close(uintptr_t db); |
| 73 | |
| 74 | WitDbStatus witdb_get(uintptr_t db, |
| 75 | const uint8_t* key, uint32_t key_len, |
| 76 | uint8_t** out_value, uint32_t* out_value_len); // NULL/0 if missing |
| 77 | |
| 78 | WitDbStatus witdb_put(uintptr_t db, |
| 79 | const uint8_t* key, uint32_t key_len, |
| 80 | const uint8_t* value, uint32_t value_len); |
| 81 | |
| 82 | WitDbStatus witdb_delete(uintptr_t db, |
| 83 | const uint8_t* key, uint32_t key_len, |
| 84 | uint8_t* out_deleted); |
| 85 | |
| 86 | WitDbStatus witdb_txn_begin(uintptr_t db, uintptr_t* out_txn); |
| 87 | WitDbStatus witdb_txn_commit(uintptr_t txn); |
| 88 | WitDbStatus witdb_txn_rollback(uintptr_t txn); |
| 89 | // txn get/put/delete — mirror db ops on txn handle |
| 90 | |
| 91 | void witdb_buffer_free(uint8_t* ptr); |
| 92 | const char* witdb_last_error_message(void); |
| 93 | ``` |
| 94 | |
| 95 | **Non-goals v1:** `scan`, SQL, indexes, async — только KV + single txn per db handle. |
| 96 | |
| 97 | --- |
| 98 | |
| 99 | ## Open semantics |
| 100 | |
| 101 | Делегирование `WitDatabase` static API (как bridge ADR-0002): |
| 102 | |
| 103 | | `create_if_missing` | password | Core call | |
| 104 | |---------------------|----------|-----------| |
| 105 | | 1 | NULL | `CreateOrOpen(path)` | |
| 106 | | 1 | set | `CreateOrOpen(path, password)` | |
| 107 | | 0 | NULL | `Open(path)` | |
| 108 | | 0 | set | `Open(path, password)` | |
| 109 | |
| 110 | `OutWit.Database.Core.BouncyCastle` linked + `EnsureRegistered()` at first `witdb_open` (Tier-2 chacha). |
| 111 | |
| 112 | **Native open policy:** `WithoutFileLocking()` — cross-process `FileLock` + `Thread.Sleep` внутри UCO entry unsafe; in-process ACID сохраняется. Для multi-process writer — bridge или shared RDBMS (ADR-0002). |
| 113 | |
| 114 | **NativeAOT hosting (bring-up):** managed-логика (`WitDatabaseBuilder.Build`, providers, JSON index metadata) **не вызывается на UCO-потоке**. `ModuleInitializer` поднимает dedicated CLR worker; `witdb_open` маршалит работу туда (`WitDbClrThread`). UTF-8 строки — `WitDbUtf8` (без `Marshal.PtrToStringUTF8` в UCO). Trim roots: `OutWit.Database.Native/trimming.xml` (сузить после contract tests). |
| 115 | |
| 116 | --- |
| 117 | |
| 118 | ## Build & artifacts |
| 119 | |
| 120 | ```bash |
| 121 | dotnet publish Sources/Core/OutWit.Database.Native/OutWit.Database.Native.csproj \ |
| 122 | -c Release -f net10.0 -r win-x64 |
| 123 | ``` |
| 124 | |
| 125 | **Runtime:** NativeAOT publish targets **net10.0** (aligned with `OutWit.Database.Core` multi-TFM). |
| 126 | |
| 127 | Output: `witdb.dll` (+ platform equivalents: `libwitdb.so`, `libwitdb.dylib`). |
| 128 | |
| 129 | RID matrix (CI later): `win-x64`, `linux-x64`, `osx-arm64`, `osx-x64`. |
| 130 | |
| 131 | --- |
| 132 | |
| 133 | ## pywitdb integration |
| 134 | |
| 135 | | Module | Роль | |
| 136 | |--------|------| |
| 137 | | `pywitdb._native.lib` | `ctypes.CDLL`, resolve DLL (`WITDB_NATIVE_PATH` → package `native/<rid>/`) | |
| 138 | | `pywitdb._native.db` | `NativeDatabase` implements `Database` | |
| 139 | | `pywitdb.open(..., backend="auto")` | native if DLL + ABI match, иначе bridge | |
| 140 | |
| 141 | Env override: `WITDB_NATIVE_PATH` = полный путь к `witdb.dll` / `libwitdb.so`. |
| 142 | |
| 143 | --- |
| 144 | |
| 145 | ## Upstream PR |
| 146 | |
| 147 | После стабилизации contract tests в fork — PR **`AI-Guiders/WitDatabase` → `dmitrat/WitDatabase`** с пакетом `OutWit.Database.Native` и `include/witdb.h`. |
| 148 | |
| 149 | --- |
| 150 | |
| 151 | ## Contract tests |
| 152 | |
| 153 | 1. **Native.Tests** (C#): P/Invoke round-trip против published `witdb.dll`. |
| 154 | 2. **pywitdb** `tests/test_native.py`: skip if DLL absent; else KV + txn. |
| 155 | 3. **Cross-lang:** C# Core writes fixture → Python native reads (Tier-1b). |
| 156 | |