Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ jobs:
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build /tmp/slotsboxmalloc/build -j
sudo cmake --install /tmp/slotsboxmalloc/build
- name: Install kvspace ABI headers (version from deps.json)
run: |
ver=$(jq -r '.kvspace' deps.json)
git clone --depth 1 --branch "$ver" https://github.com/array2d/kvspace.git /tmp/kvspace
sudo mkdir -p /usr/local/include
sudo cp -R /tmp/kvspace/include/. /usr/local/include/
- name: 构建
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ jobs:
run: ./ci/deps.sh
- name: 构建
run: make
- name: Run C tests
run: make test
- name: Run Python wrapper tests
run: KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ jobs:
v="${RELEASE_TAG:-${GITHUB_REF_NAME}}"
d="kvspace-c-abi-$v-linux-x86_64"
mkdir -p "$d/include" "$d/lib"
# 头:本仓无公共头(内部头在 src/),权威 ABI 头在 kvspace 仓 → 递归整目录
git clone --depth 1 https://github.com/array2d/kvspace.git /tmp/kvspace-hdr
# Copy authoritative ABI headers from the pinned kvspace tag in deps.json.
kvspace_ver=$(jq -r '.kvspace' deps.json)
test -n "$kvspace_ver" && test "$kvspace_ver" != null
git clone --depth 1 --branch "$kvspace_ver" https://github.com/array2d/kvspace.git /tmp/kvspace-hdr
cp -R /tmp/kvspace-hdr/include/. "$d/include/"
# 库:扫构建目录的所有 .so*/.dylib(-P 保符号链接)
find build -maxdepth 3 \( -name 'lib*.so*' -o -name 'lib*.dylib' \) -exec cp -P {} "$d/lib/" \;
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
build/
build-test/
cmake-build-*/
*.o
*.a
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ if(KVSPACE_BUILD_TESTS)
target_include_directories(test_codec_abi PRIVATE src)
target_link_libraries(test_codec_abi PRIVATE kvspace-c)
add_test(NAME codec_abi COMMAND test_codec_abi)
add_executable(test_abi_contract tests/test_abi_contract.c)
target_include_directories(test_abi_contract PRIVATE src)
target_link_libraries(test_abi_contract PRIVATE kvspace-c)
add_test(NAME abi_contract COMMAND test_abi_contract)
add_executable(test_set_part_fastpath tests/test_set_part_fastpath.c)
target_include_directories(test_set_part_fastpath PRIVATE src)
target_link_libraries(test_set_part_fastpath PRIVATE kvspace-c)
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
.PHONY: build clean
.PHONY: build test clean

build:
@mkdir -p build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$$(nproc)

# Tests need the kvspace ABI headers (<kvspace/kvspace.h>) on the include path.
test:
@mkdir -p build-test && cd build-test && \
cmake -DCMAKE_BUILD_TYPE=Debug -DKVSPACE_BUILD_TESTS=ON .. && \
make -j$$(nproc) && ctest --output-on-failure

clean:
rm -rf build
rm -rf build build-test
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ Backend: `shm://` — single file-backed mmap block (ART-tree key index + slotsb

```bash
make # → build/libkvspace-c.so
make test # → build-test/, runs ctest (needs the kvspace ABI headers)
```

Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only + `.so` dual-mode libraries).
Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only, compiled into this library).
Tests additionally include the authoritative ABI header `<kvspace/kvspace.h>` from [`kvspace`](../kvspace) (`include/`).
`ci/deps.sh` installs all three at the versions pinned in `deps.json`.

## API

Two surfaces:

1. **Native C API** (`kvspaceShm*`) — `include/kvspace/kvspace.h`:
1. **Native C API** (`kvspaceShm*`) — `src/kvspace_shm.h` (internal header):
`kvspaceShmOpen/Close`, `kvspaceShmGet/Set`, `kvspaceShmList`, `kvspaceShmDel/Deltree`, `kvspaceShmMkindex`, `kvspaceShmExtindex/Delextindex`, `kvspaceShmNotify/Watch`.

2. **durable C ABI** — byte-compatible with `kvspace-durable` (`src/durable_abi.c`):
Expand All @@ -33,24 +36,30 @@ Two surfaces:

## XValue

Fixed 8-byte head wire format (`#566`), read/written by direct `memcpy` of the
`kvspaceXvalueHead` struct:
Fixed 8-byte head (`#566`) followed by the body and an attr region (`#571`), stored as one value under one key:

```
[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8]
[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8][attr @8+len]
attr = [attr_len:u16 le][records] attr_len includes itself; empty attr = 2
record = [tag:u8][len:u16 le][data] tag 1 = langtype (kindexpr); 2+ reserved
total = 8 + len + attr_len
```

- storetype selects short, slack, large, or extension storage; ptr marks pointer/ext.
- langtype (kindexpr) is not in the head — it lives in the `x‥langtype` attr key.
- The head is built with `kvspaceXvalueHeadNew*` constructors.
- langtype (kindexpr) is the tag-1 record of the attr region; no record = untyped.
- Values are built with the `kvspaceXvalueHeadNew*` constructors. `kvspaceSetValue`
rejects values whose attr region is missing or runs past the value.
- Store format version 6 (this wire). Stores written by older versions are refused at open; rebuild them.

## Tutorial
## Tests

```bash
python3 tutorial/test.py
make test # C tests (ctest)
KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py
(cd rust && cargo test) # links ../build
```

Cases: `01_basic.c`, `02_cpp.cpp`, `03_python.py`, `04_rust.rs`, `05_integrity.c`, `06_multiprocess.c`.
Cross-language tutorials live in the [`kvspace`](../kvspace) repo (`tutorial-c/`).

## Language wrappers

Expand Down
1 change: 1 addition & 0 deletions deps.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"blockmalloc": "v0.1.5",
"kvspace": "v0.3.23",
"slotsboxmalloc": "v0.1.7"
}
140 changes: 93 additions & 47 deletions py/kvspace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,31 @@ def _bind(fn, argtypes, restype):
_bind(_lib.kvspaceShmSet, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int)
_bind(_lib.kvspaceShmDel, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int)
_bind(_lib.kvspaceShmDeltree, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int)
_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int)
_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int)
_bind(_lib.kvspaceShmList, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_bool, ctypes.c_int,
ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32)], ctypes.c_int)
_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int)
_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p,
ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int)
_bind(_lib.kvspaceShmDelextindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int)
_bind(_lib.kvspaceShmNotify, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int)
_bind(_lib.kvspaceShmWatch, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_int32)], ctypes.POINTER(ctypes.c_uint8))

# ── 权威 head 构造器绑定(kvspace ABI,无 handle)──────────────────
# #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy
# struct + body——构造走 C 构造器 kvspaceXvalueHeadNew*,读取本地 memcpy 固定头 + 字段访问。
# #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at
# 8+len ([attr_len:u16][records], record = [tag:u8][len:u16][data], tag 1 = langtype).
# Constructed via C kvspaceXvalueHeadNew*; parsed locally on read.

_U8P = ctypes.POINTER(ctypes.c_uint8)
_U8PP = ctypes.POINTER(_U8P)
_U64P = ctypes.POINTER(ctypes.c_uint64)
_bind(_lib.kvspaceXvalueHeadNewScalar,
[ctypes.c_uint8, _U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int)
[ctypes.c_uint8, _U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int)
_bind(_lib.kvspaceXvalueHeadNewSlack,
[_U8P, ctypes.c_uint64, ctypes.c_uint64, _U8PP, _U64P], ctypes.c_int)
[_U8P, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int)
_bind(_lib.kvspaceXvalueHeadNewPtr,
[_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int)
[_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int)
_bind(_lib.kvspaceXvalueHeadNewExt,
[_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int)
[_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int)

ST_SHORT, ST_SLACK, ST_LARGE, ST_EXT = 0, 1, 2, 3
PTR_INLINE, PTR_PTR, PTR_EXT = 0, 1, 2
Expand All @@ -61,8 +63,11 @@ def _bind(fn, argtypes, restype):


# ── XValue helpers ────────────────────────────────────────────────
# 构造走 C 构造器(输出 frontend malloc,_take 拷出后 free);读取本地
# memcpy 固定头 + 字段访问。
# Construct via C constructors (_take copies and frees malloc buffer); parse header + attr locally.
# Empty langtype = untyped (empty attr).

ATTR_TAG_LANGTYPE = 1


def _take(out, total: ctypes.c_uint64) -> bytes:
data = ctypes.string_at(out, total.value)
Expand All @@ -74,61 +79,95 @@ def _new(fn, *args) -> bytes:
out = _U8P()
total = ctypes.c_uint64()
if fn(*args, ctypes.byref(out), ctypes.byref(total)):
raise ValueError(f"{fn.__name__} 构造失败")
raise ValueError(f"{fn.__name__} failed")
return _take(out, total)


def _xv_scalar(raw: bytes) -> bytes:
buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None
return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT,
ctypes.cast(buf, _U8P) if buf is not None else None, len(raw))
def _u8buf(raw: bytes):
if not raw:
return None
return ctypes.cast((ctypes.c_uint8 * len(raw)).from_buffer_copy(raw), _U8P)


def _xv_slack(raw: bytes) -> bytes:
buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None
return _new(_lib.kvspaceXvalueHeadNewSlack,
ctypes.cast(buf, _U8P) if buf is not None else None, len(raw), len(raw))
def _xv_scalar(raw: bytes, langtype: str) -> bytes:
return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT, _u8buf(raw), len(raw),
langtype.encode())


def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]:
"""读固定头 → (ref, storetype, body)。langtype 不在 head,kind 无从解析。"""
if not data or len(data) < HEAD_LEN:
return (0, 0, b"")
def _xv_slack(raw: bytes, langtype: str) -> bytes:
return _new(_lib.kvspaceXvalueHeadNewSlack, _u8buf(raw), len(raw), len(raw),
langtype.encode())


def _split_wire(data: Optional[bytes]):
"""Validate and split wire into (storetype, ptr, body, attr); returns None if malformed."""
if not data or len(data) < HEAD_LEN + 2:
return None
body_len = int.from_bytes(data[:6], "little")
storetype, ptr = data[6], data[7]
if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len != len(data):
if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len + 2 > len(data):
return None
a = HEAD_LEN + body_len
attr_len = int.from_bytes(data[a:a + 2], "little")
if attr_len < 2 or a + attr_len != len(data):
return None
return storetype, ptr, data[HEAD_LEN:a], data[a:a + attr_len]


def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]:
"""Read fixed header -> (ref, storetype, body) excluding attr; returns (0, 0, b"") if invalid."""
parts = _split_wire(data)
if parts is None:
return (0, 0, b"")
storetype, ptr, body, _ = parts
ref = REF_PTR if ptr == PTR_PTR else REF_EXT if ptr == PTR_EXT else REF_INLINE
return (ref, storetype, data[HEAD_LEN:])
return (ref, storetype, body)


def xv_langtype(data: Optional[bytes]) -> str:
"""Read attr tag=1 (langtype) record; returns "" if untyped or invalid."""
parts = _split_wire(data)
if parts is None:
return ""
attr = parts[3]
p = 2
while p + 3 <= len(attr):
tag, rlen = attr[p], int.from_bytes(attr[p + 1:p + 3], "little")
if p + 3 + rlen > len(attr):
return ""
if tag == ATTR_TAG_LANGTYPE:
return attr[p + 3:p + 3 + rlen].decode()
p += 3 + rlen
return ""


def xv_int(v: int) -> bytes:
return _xv_scalar(struct.pack("<q", v))
def xv_int(v: int, langtype: str = "") -> bytes:
return _xv_scalar(struct.pack("<q", v), langtype)


def xv_float(v: float) -> bytes:
return _xv_scalar(struct.pack("<d", v))
def xv_float(v: float, langtype: str = "") -> bytes:
return _xv_scalar(struct.pack("<d", v), langtype)


def xv_str(s: str) -> bytes:
return _xv_slack(s.encode())
def xv_str(s: str, langtype: str = "") -> bytes:
return _xv_slack(s.encode(), langtype)


def xv_index() -> bytes:
"""目录值(空 body 的 SHORT 头)。"""
return _xv_scalar(b"")
"""Directory value (empty-body SHORT header, untyped)."""
return _xv_scalar(b"", "")


def xv_link(target: str) -> bytes:
"""指针值(ptr 位):body=目标 key。langtype 由调用方写 x‥langtype attr key。"""
buf = (ctypes.c_uint8 * len(target)).from_buffer_copy(target.encode())
return _new(_lib.kvspaceXvalueHeadNewPtr, buf, len(target))
def xv_link(target: str, langtype: str = "") -> bytes:
"""Pointer value (ptr=PTR): body=target key, langtype=target constraint ("" = unconstrained)."""
t = target.encode()
return _new(_lib.kvspaceXvalueHeadNewPtr, _u8buf(t), len(t), langtype.encode())


def xv_ext(locator: str) -> bytes:
"""外部定位符(EXT class):body=locator。langtype 由调用方写 x‥langtype attr key。"""
buf = (ctypes.c_uint8 * len(locator)).from_buffer_copy(locator.encode())
return _new(_lib.kvspaceXvalueHeadNewExt, buf, len(locator))
def xv_ext(locator: str, langtype: str = "") -> bytes:
"""External locator (EXT class): body=locator."""
loc = locator.encode()
return _new(_lib.kvspaceXvalueHeadNewExt, _u8buf(loc), len(loc), langtype.encode())


# ── KVSpace ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -177,22 +216,29 @@ def deltree(self, prefix: str):

# ── Directory ────────────────────────────────────────────

def mkindex(self, path: str):
_lib.kvspaceShmMkindex(self._kv, path.encode())
def mkindex(self, path: str, capacity: int = 0):
_lib.kvspaceShmMkindex(self._kv, path.encode(), capacity)

def list(self, prefix: str, resolve: bool = True) -> list[str]:
out = ctypes.c_void_p()
oc = ctypes.c_int32(0)
_lib.kvspaceShmList(self._kv, prefix.encode(), False, 1 if resolve else 0, ctypes.byref(out), ctypes.byref(oc))
if oc.value == 0:
if not out.value:
return []
ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_char_p))
return [ptrs[i].decode() for i in range(oc.value)]
# Copy and free the C-allocated name array.
ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_void_p))
names = [ctypes.string_at(ptrs[i]).decode() for i in range(oc.value)]
for i in range(oc.value):
_libc.free(ptrs[i])
_libc.free(out)
return names

# ── Link / ExtIndex ──────────────────────────────────────

def extindex(self, path: str, extpath: str):
_lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode())
err = ctypes.create_string_buffer(256)
if _lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode(), err, len(err)):
raise RuntimeError(err.value.decode() or f"extindex({path}, {extpath}) failed")

def delextindex(self, path: str):
_lib.kvspaceShmDelextindex(self._kv, path.encode())
Loading
Loading