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: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ jobs:
v="${GITHUB_REF_NAME}"
d="kvspace-durable-abi-$v-linux-x86_64"
mkdir -p "$d/include" "$d/lib"
# 头:本仓为 Rust(无 C 头),权威 ABI 头在 kvspace 仓 → 递归整目录
git clone --depth 1 https://github.com/array2d/kvspace.git /tmp/kvspace-hdr
# Bundle ABI headers pinned to the 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/"
# 库:扫 target/release 的 .so*/.dylib(-P 保符号链接)
find target/release -maxdepth 2 \( -name 'lib*.so*' -o -name 'lib*.dylib' \) -exec cp -P {} "$d/lib/" \;
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,28 @@ Crate types: `rlib`, `staticlib`, `cdylib` (`libkvspace_durable.so`).
C ABI exported from the cdylib (`src/ffi.rs`):

- lifecycle: `kvspaceConnect`, `kvspaceClose`
- KV read/write: `kvspaceGet` (borrow), synchronous `kvspaceSetValue`, `kvspaceWriteInPlace`, `kvspaceWriteNewPlace`
- enumerate / delete / copy: `kvspaceListLen`, `kvspaceListAt`, `kvspaceDel`, `kvspaceDelTree`, `kvspaceCp`, `kvspaceCpTree`
- KV read/write: `kvspaceGet` (borrow), `kvspaceGetPart`, `kvspaceGetHead`, synchronous `kvspaceSetValue`, `kvspaceSetPart`, `kvspaceWriteInPlace`, `kvspaceWriteNewPlace`
- enumerate / delete / copy: `kvspaceListLen`, `kvspaceListAt`, `kvspaceDel`, `kvspaceDelTree`, `kvspaceCp`, `kvspaceCpTree`, `kvspaceCpList`
- directories / extindex: `kvspaceMkindex`, `kvspaceMkindexExt`, `kvspaceRmindexExt`
- watch / clear: `kvspaceWatch`, `kvspaceClear`
- XValue codec (frontend malloc, caller `free()`s): `kvspaceXvalueEncode`, `kvspaceXvalueEncodeMode`, `kvspaceDecodeHead`, `kvspaceNewPtr`, `kvspaceNewChar`, `kvspaceNewBool`, `kvspaceNewInt64`, `kvspaceNewFloat64`
- XValue constructors (caller `free()`s; same signatures and bytes as the frontend and `kvspace-c`, `langtype` is written verbatim): `kvspaceNewPtr`, `kvspaceNewChar`, `kvspaceNewBool`, `kvspaceNewInt64`, `kvspaceNewFloat64`
- shm-only views, always `-2` here (callers fall back to the calls above): `kvspaceXvalueBodyView`, `kvspaceView*`

The same ABI is implemented by `kvspace-c` (`shm://`), so a consumer (e.g. the kvlang layout) switches backends by DSN only, with no code change.

## XValue

The headlenpow wire format is byte-identical across this backend, `kvspace-c`, and the `kvspace` frontend. Its 18-byte prefix is `[headlenpow:u8][flags:u8][a:u64 LE][b:u64 LE]`. The UTF-8 langtype occupies the rest of the `2^headlenpow` head, followed by the body. `flags` uses its low two bits for the storage class and bit 2 for a pointer.
The wire format is byte-identical across this backend, `kvspace-c`, and the `kvspace` frontend (`kvspace/xvalue_head.h`), and a value is always one key:

- Class 0 stores short fixed values, `None`, maps, structs, and directory markers. `None` has an empty langtype; map members and directory children are enumerated from physical key prefixes.
- Class 1 stores a byte length and reserved capacity (`a`, `b`) for strings and code slots. UTF-8 records the code point count in its langtype and the byte count in `a`.
- Class 2 stores tensor element count and width. Its langtype carries the dimensions.
- Class 3 stores an external locator, with the target's actual langtype. Pointer values use class 1 with the pointer bit set and a target key in the body.
```
[len:u48 LE @0-5][storetype:u8 @6][ptr:u8 @7][body: len bytes @8][attr]
attr = [attr_len:u16 LE (includes itself)][record...] empty attr = [2,0]
record = [tag:u8][len:u16 LE][data] tag 1 = langtype, 2+ reserved
```

`ro` and `vid` live at `/.kvspace-meta/<hex key>` as a separate XValue. Writes and copies preserve the sidecar; its absence means `ro=false, vid=0`.
- `storetype` 0 short fixed values, `None`, maps, structs and directory markers; 1 variable length (strings, code slots, pointer paths); 2 tensors (the langtype carries the dimensions); 3 external locators (`@ext`).
- `ptr` 0 inline, 1 pointer (body = target key, langtype = target kindexpr), 2 `@ext` (body = locator).
- An empty langtype means `None` for inline values. Map members and directory children are enumerated from physical key prefixes.

## Tutorial

Expand Down
3 changes: 3 additions & 0 deletions deps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"kvspace": "v0.3.23"
}
122 changes: 62 additions & 60 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use std::time::Duration;
use crate::kvspace::{KVPair, KVSpace};
use crate::kvspace_common::{
is_descendant, is_frame_operand_key, join_path, sep_path, split_index, strip_dir_suf,
validate_ptr, watch_value,
subtree_base, watch_value,
};
use crate::r#const::*;
use crate::store::KVStore;
use crate::xvalue::{
from_wire, from_wire_head, is_ptr, ptr_target, XValue, REF_EXT, REF_PTR,
from_wire, from_wire_head, is_ptr, ptr_target, XValue, REF_EXT, REF_INLINE, REF_PTR, ST_EXT,
ST_SHORT,
};

pub struct Backend<S: KVStore> {
Expand Down Expand Up @@ -39,13 +40,14 @@ impl<S: KVStore> Backend<S> {

fn resolve_path(&self, path: &str) -> String {
let mut path = path.to_string();
loop {
for _ in 0..16 {
let (resolved, changed) = self.resolve_one(&path);
if !changed {
return resolved;
}
path = resolved;
}
path
}

fn resolve_parent(&self, path: &str) -> String {
Expand All @@ -71,7 +73,19 @@ impl<S: KVStore> Backend<S> {
if path == PATH_SEP {
return (path.to_string(), false);
}
let trimmed = path.trim_matches('/');
let dir_suf = if path.ends_with(OBJ_SEP) {
Some(OBJ_SEP)
} else if path.ends_with(DIR_INDEX_SUF) {
Some(DIR_INDEX_SUF)
} else {
None
};
let clean = if dir_suf.is_some() {
strip_dir_suf(path)
} else {
path
};
let trimmed = clean.trim_matches('/');
let parts: Vec<&str> = if trimmed.is_empty() {
Vec::new()
} else {
Expand All @@ -92,10 +106,17 @@ impl<S: KVStore> Backend<S> {
let v = from_wire(data);
if is_ptr(&v) {
let target = ptr_target(&v);
if i + 1 < parts.len() {
return (join_path(&target, &parts[i + 1..].join("/")), true);
let mut next = if i + 1 < parts.len() {
join_path(&target, &parts[i + 1..].join("/"))
} else {
target
};
if let Some(suf) = dir_suf {
if !Self::is_dir(&next) {
next.push_str(suf);
}
}
return (target, true);
return (next, true);
}
}
}
Expand All @@ -120,7 +141,7 @@ impl<S: KVStore> Backend<S> {
let end = rest.find(['/', '·']).unwrap_or(rest.len());
if end > 0 {
let name = &rest[..end];
// RUNTIME_MEMBER_SEP(‥)前缀键是系统变量/langtype attr key,List 时隐藏。
// Hide runtime system keys containing RUNTIME_MEMBER_SEP (‥) from List.
if (dir != PATH_SEP || name != META_ROOT_NAME) && !name.contains(RUNTIME_MEMBER_SEP)
{
let direct_dir =
Expand Down Expand Up @@ -328,13 +349,17 @@ impl<S: KVStore> KVSpace for Backend<S> {
.ok_or_else(|| format!("invalid XValue at {key}"))?;
let attr = crate::headlenpow::attr_len(&raw, head.len as usize)
.ok_or_else(|| format!("invalid XValue at {key}"))?;
if head.len as usize + crate::headlenpow::PREFIX + attr as usize != raw.len() {
if attr < 2 {
return Err(format!("invalid XValue at {key}"));
}
// 指针校验用声明的 target_kindexpr(val 为 Ptr 时可得;raw 直写无类型则跳过)。
if let XValue::Ptr(ptr) = &pair.val {
validate_ptr(self, &ptr.target, &ptr.target_kindexpr)?;
let total = (head.len as usize)
.checked_add(crate::headlenpow::PREFIX)
.and_then(|x| x.checked_add(attr as usize))
.ok_or_else(|| format!("invalid XValue at {key}"))?;
if total > raw.len() {
return Err(format!("invalid XValue at {key}"));
}
let raw = &raw[..total];
let (parent, _, _) = split_index(&key);
if parent.ends_with(OBJ_SEP) {
let base = strip_dir_suf(&parent);
Expand All @@ -355,7 +380,7 @@ impl<S: KVStore> KVSpace for Backend<S> {
}
}
// 单 key 整体写(类型在值 attr 区):天然原子,无 attr key、无写序。
self.store.set(&key, &raw);
self.store.set(&key, raw);
}
Ok(())
}
Expand Down Expand Up @@ -403,25 +428,6 @@ impl<S: KVStore> KVSpace for Backend<S> {
result
}

fn list_len(&mut self, prefix: &str, expand_ext: bool, resolve: bool) -> i32 {
self.list(prefix, expand_ext, resolve).len() as i32
}

fn list_at(
&mut self,
prefix: &str,
idx: i32,
expand_ext: bool,
resolve: bool,
) -> Option<String> {
if idx < 0 {
return None;
}
self.list(prefix, expand_ext, resolve)
.into_iter()
.nth(idx as usize)
}

fn del(&mut self, keys: &[String]) -> Result<(), String> {
for key in keys {
if crate::r#const::is_reserved(key) {
Expand Down Expand Up @@ -454,36 +460,33 @@ impl<S: KVStore> KVSpace for Backend<S> {
if crate::r#const::is_reserved(prefix) {
return Err(format!("reserved metadata key: {prefix}"));
}
let mut link_key = prefix;
if Self::is_dir(link_key) && link_key != PATH_SEP {
link_key = strip_dir_suf(prefix);
}
if let Some(data) = self.store.get(link_key) {
let head = from_wire_head(&data);
if head.is_ptr() {
return self.del(&[prefix.to_string()]);
}
}

let resolved = self.resolve_path(prefix);
// Resolve links only on parent components (POSIX rm semantics); if the final
// component is itself a link, delete the link rather than its target subtree.
let resolved = self.resolve_parent(prefix);
if crate::r#const::is_reserved(&resolved) {
return Err(format!("reserved metadata key: {resolved}"));
}
let mut scan = resolved.clone();
if Self::is_dir(&scan) && scan != PATH_SEP {
scan.pop();
let base = subtree_base(&resolved);
if !base.is_empty() {
if let Some(data) = self.store.get(base) {
if from_wire_head(&data).is_ptr() {
return self.del(std::slice::from_ref(&resolved));
}
}
}

// Delete base and all descendants bounded by '/', '·', or '‥' (via is_subtree_key).
let scan = if base.is_empty() { PATH_SEP } else { base };
let keys: Vec<_> = self
.store
.scan_keys(&scan)
.scan_keys(scan)
.into_iter()
.filter(|key| !crate::r#const::is_reserved(key))
.collect();

let mut del_keys: Vec<&str> = Vec::with_capacity(keys.len() + 1);
del_keys.push(&resolved);
del_keys.extend(keys.iter().map(String::as_str));
self.store.del(&del_keys);
let del_keys: Vec<&str> = keys.iter().map(String::as_str).collect();
if !del_keys.is_empty() {
self.store.del(&del_keys);
}

Ok(())
}
Expand All @@ -497,7 +500,7 @@ impl<S: KVStore> KVSpace for Backend<S> {
if raw.is_empty() {
return Err(format!("Cp: source not found: {}", src));
}
// attr key 与值同址:读解析后的物理 src 路径的类型。
// Copy raw wire bytes verbatim (langtype lives in the value's attr trailer).
let v = from_wire(&raw);
self.set(&[KVPair {
key: dst.to_string(),
Expand Down Expand Up @@ -583,9 +586,8 @@ impl<S: KVStore> KVSpace for Backend<S> {
let _ = self.del_tree(&dst_base);
for k in &keys {
let suffix = &k[src_base.len()..];
// Copy the base(含其 langtype attr key)、直接 · 成员(含成员 attr key)与 ext 目录。
// Copy base value, direct '·' members, and ext directories (attrs travel with values).
let one_level = suffix.is_empty()
|| suffix == format!("{RUNTIME_MEMBER_SEP}langtype")
|| (suffix.starts_with(OBJ_SEP) && {
let rest = &suffix[OBJ_SEP.len()..];
let member = rest.strip_suffix(PATH_SEP).unwrap_or(rest);
Expand All @@ -612,8 +614,8 @@ impl<S: KVStore> KVSpace for Backend<S> {
}
let _ = capacity;
let resolved = self.resolve_path(path);
// 固定头空目录 marker:storetype 0、ptr 0、空 body;类型 "lib" 在值 attr 区。
let value = crate::headlenpow::to_wire(0, 0, &[], "lib")
// Directory marker: ST_SHORT, inline, empty body, langtype "lib" in attr.
let value = crate::headlenpow::to_wire(ST_SHORT, REF_INLINE, &[], "lib")
.ok_or_else(|| format!("Mkindex: invalid directory {resolved}"))?;
if self.store.get(&resolved).is_none() {
self.store.set(&resolved, &value);
Expand All @@ -637,8 +639,8 @@ impl<S: KVStore> KVSpace for Backend<S> {
return Err(format!("ExtIndex target missing: {ext_path}"));
}
let langtype = self.langtype_of(ext_path);
// 扩展定位:storetype 3(ST_EXT)+ ptr 2(PTR_EXT)+ 定位串 body + 类型 attr 区。
let value = crate::headlenpow::to_wire(3, 2, ext_path.as_bytes(), &langtype)
// External locator: ST_EXT + REF_EXT + locator body + target langtype in attr.
let value = crate::headlenpow::to_wire(ST_EXT, REF_EXT, ext_path.as_bytes(), &langtype)
.ok_or_else(|| format!("ExtIndex locator invalid: {ext_path}"))?;
self.store.set(&resolved, &value);
Ok(())
Expand Down
1 change: 0 additions & 1 deletion src/const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub const PATH_SEP: &str = "/"; // 路径分隔符
pub const META_ROOT_NAME: &str = ".kvspace-meta"; // 保留区(不再有 meta sidecar,仅守卫防业务写)
pub const DIR_INDEX_SUF: &str = "/"; // 目录索引键后缀(尾斜杠 = 目录,必须以 / 开头的 key 保证不冲突)
pub const RUNTIME_MEMBER_SEP: &str = "‥"; // 运行时保留字段前缀(U+2025)——‥ 的唯一定义处,List 时隐藏
pub const EXT_INDEX_HEAD: &str = "…"; // Directory display marker.

/// `/.kvspace-meta` 保留区判定(原 metadata::is_reserved,随 meta 删除迁至常量模块)。
pub fn is_reserved(key: &str) -> bool {
Expand Down
Loading
Loading