✨ feat: 灵台引擎五神齐备与 Go/Rust 官方绑定(WP-15)

- C++ 新增四模块(modules.cpp,19 个导出符号全部 hj_*):
  小周天 hj_stream:流会话(sequence/delta_index 单调)与 content.block.delta envelope 确定性编码(重放字节一致)
  烛照 hj_policy:URL 协议白名单(拒 file/javascript/data)、路径授权根与 .. 段拒绝、幂等键非空≤128、四维决策表(autonomy×risk→allow/ask/deny,非法输入 deny fail-closed)
  洛书 hj_plan:预算核算(达限/负数拒绝)
  河图 hj_recall:确定性重排(词频×稳定排序,canonical JSON 输出;非法输入 PARSE_FAILED)
- doctest 22 用例 20135 断言全绿(含每模块 golden 与确定性双跑)
- Go 绑定(cgo 自包含编译引擎源码):Engine RAII/TransformMarkdown/ValidateURL/ValidatePath/Decide/CheckBudget/RecallRerank;go test 3 用例通过(transform/rerank golden 与 C++ 字节一致)
- Rust 绑定(bindgen 生成 + 安全包装):Engine RAII(Drop 释放)、Error 枚举映射全部 HjStatus、Send/Sync(HjEngine 线程安全);cargo test 3 用例通过(同 golden)
- 三端(C++/Go/Rust)与 Python golden 一致:同一 markdown 输入在四端输出字节相同(跨端一致性首次全语言验证)
This commit is contained in:
chenxin committed 2026-09-12 13:54:12 +08:00
1 parent 99a7789432
commit a39cec5287
15 files changed
+1205 -3

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
//! 灵台引擎官方 Rust 绑定(bindgen 生成 + 安全包装,ADR-016/017)。
//! 薄封装:句柄 RAII、错误转 `Result`;禁止裸指针外泄(契约 lingtai-ffi 第 8 节)。
#![allow(clippy::missing_safety_doc)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use std::ffi::{c_char, c_void, CStr, CString};
use std::fmt;
#[derive(Debug)]
pub enum Error {
InvalidArgument(String),
OutOfMemory,
UnsupportedAbi,
HandleInvalid,
ParseFailed(String),
LimitExceeded(String),
PolicyDenied(String),
Internal(String),
Unknown(i32, String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidArgument(m) => write!(f, "参数不合法:{m}"),
Error::OutOfMemory => write!(f, "内存分配失败"),
Error::UnsupportedAbi => write!(f, "ABI major 不匹配"),
Error::HandleInvalid => write!(f, "句柄无效"),
Error::ParseFailed(m) => write!(f, "解析失败:{m}"),
Error::LimitExceeded(m) => write!(f, "超限:{m}"),
Error::PolicyDenied(m) => write!(f, "策略拒绝:{m}"),
Error::Internal(m) => write!(f, "内部错误:{m}"),
Error::Unknown(code, m) => write!(f, "未知错误 {code}:{m}"),
}
}
}
impl std::error::Error for Error {}
fn map_status(st: HjStatus, message: String) -> Result<(), Error> {
match st {
HjStatus_HJ_OK => Ok(()),
HjStatus_HJ_ERR_INVALID_ARGUMENT => Err(Error::InvalidArgument(message)),
HjStatus_HJ_ERR_OUT_OF_MEMORY => Err(Error::OutOfMemory),
HjStatus_HJ_ERR_UNSUPPORTED_ABI => Err(Error::UnsupportedAbi),
HjStatus_HJ_ERR_HANDLE_INVALID => Err(Error::HandleInvalid),
HjStatus_HJ_ERR_PARSE_FAILED => Err(Error::ParseFailed(message)),
HjStatus_HJ_ERR_LIMIT_EXCEEDED => Err(Error::LimitExceeded(message)),
HjStatus_HJ_ERR_POLICY_DENIED => Err(Error::PolicyDenied(message)),
HjStatus_HJ_ERR_INTERNAL => Err(Error::Internal(message)),
other => Err(Error::Unknown(other as i32, message)),
}
}
fn last_error() -> String {
unsafe {
let ptr = hj_last_error();
if ptr.is_null() {
String::new()
} else {
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
}
/// 引擎句柄(RAII:Drop 自动释放)。
pub struct Engine {
ptr: *mut HjEngine,
}
impl Engine {
pub fn new() -> Result<Self, Error> {
let mut ptr: *mut HjEngine = std::ptr::null_mut();
let st = unsafe { hj_engine_new(HJ_ABI_VERSION, &mut ptr) };
map_status(st, last_error())?;
Ok(Engine { ptr })
}
/// Markdown → canonical ContentBlock JSON。
pub fn transform_markdown(&self, input: &str) -> Result<String, Error> {
let cin = CString::new(input).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let mut buf: *mut HjBuffer = std::ptr::null_mut();
let st = unsafe {
hj_transform_markdown(self.ptr, cin.as_ptr(), cin.as_bytes().len(), &mut buf)
};
map_status(st, last_error())?;
take_buffer(buf)
}
/// 候选重排(确定性)。
pub fn recall_rerank(&self, candidates_json: &str) -> Result<String, Error> {
let cin =
CString::new(candidates_json).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let mut buf: *mut HjBuffer = std::ptr::null_mut();
let st = unsafe { hj_recall_rerank(cin.as_ptr(), &mut buf) };
map_status(st, last_error())?;
take_buffer(buf)
}
}
impl Drop for Engine {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { hj_engine_free(self.ptr) };
}
}
}
// Send/Sync:HjEngine 线程安全(契约第 4 节)
unsafe impl Send for Engine {}
unsafe impl Sync for Engine {}
fn take_buffer(buf: *mut HjBuffer) -> Result<String, Error> {
if buf.is_null() {
return Err(Error::Internal("缓冲为空".into()));
}
unsafe {
let data = hj_buffer_data(buf) as *const c_char;
let len = hj_buffer_len(buf);
let slice = std::slice::from_raw_parts(data as *const u8, len);
let out = String::from_utf8_lossy(slice).into_owned();
hj_buffer_free(buf);
Ok(out)
}
}
/// URL 协议校验(仅 http/https)。
pub fn validate_url(url: &str) -> Result<(), Error> {
let cu = CString::new(url).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let st = unsafe { hj_policy_validate_url(cu.as_ptr()) };
map_status(st, last_error())
}
/// 路径约束(授权根内、拒绝 ..)。
pub fn validate_path(path: &str, root: &str) -> Result<(), Error> {
let cp = CString::new(path).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let cr = CString::new(root).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let st = unsafe { hj_policy_validate_path(cp.as_ptr(), cr.as_ptr()) };
map_status(st, last_error())
}
/// 决策表(契约 autonomy_level × risk)。
pub fn decide(autonomy: i32, risk: i32) -> i32 {
unsafe { hj_policy_decide(autonomy, risk) }
}
/// 预算核算。
pub fn check_budget(used: i64, maximum: i64) -> Result<(), Error> {
let st = unsafe { hj_plan_check_budget(used, maximum) };
map_status(st, last_error())
}
/// 解析器版本。
pub fn transform_version() -> String {
unsafe { CStr::from_ptr(hj_transform_version()).to_string_lossy().into_owned() }
}