mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 05:16:44 +08:00
refactor(ffi): 降级 FFI 为可选兼容层并整理文档
将 bat-ffi 明确收敛为无状态 C ABI 兼容层,默认集成路径改为 bat --json 进程边界或未来稳定 SDK。 同步 README、当前状态、项目计划、架构文档、开发指南和缺口清单,移除 FFI 作为主集成边界的表述。 新增 AGENTS.md 和 CONTRIBUTING.md,压缩 CLAUDE.md 为兼容入口,并归档阶段性报告、同步 .gitignore 规则。
This commit is contained in:
+118
-40
@@ -1,8 +1,9 @@
|
||||
//! # BAT FFI
|
||||
//! # BAT FFI compatibility layer
|
||||
//!
|
||||
//! Go 和 Rust 之间的 FFI 绑定层
|
||||
//!
|
||||
//! 导出 C ABI 接口供 Go 通过 CGO 调用
|
||||
//! Optional C ABI compatibility layer for coarse-grained JSON helpers.
|
||||
//! Production orchestration should prefer the `bat --json` process boundary or a
|
||||
//! future stable SDK. This crate intentionally stays stateless: it must not own
|
||||
//! downloader state, daemon lifecycle, CAS handles, or long-lived resources.
|
||||
|
||||
#![warn(clippy::all)]
|
||||
|
||||
@@ -17,7 +18,7 @@ use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
/// FFI 版本号
|
||||
/// FFI compatibility API version.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Sync snapshot JSON 输入。
|
||||
@@ -111,13 +112,13 @@ struct SyncPlanView {
|
||||
delta: SyncDeltaView,
|
||||
}
|
||||
|
||||
/// 获取版本号(C ABI)
|
||||
/// Returns the compatibility API version as an owned C string.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bat_version() -> *const c_char {
|
||||
c_string(VERSION).into_raw()
|
||||
}
|
||||
|
||||
/// 释放 C 字符串内存
|
||||
/// Frees C strings allocated by this crate.
|
||||
///
|
||||
/// # Safety
|
||||
/// 调用者必须确保:
|
||||
@@ -130,7 +131,7 @@ pub unsafe extern "C" fn bat_free_string(s: *mut c_char) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 Addressables Manifest,并返回 JSON summary。
|
||||
/// Parses an Addressables manifest and returns a stateless JSON summary.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bat_manifest_inspect_json(raw_json: *const c_char) -> *mut c_char {
|
||||
let result = (|| -> Result<ManifestInspectView, String> {
|
||||
@@ -170,7 +171,7 @@ pub extern "C" fn bat_manifest_inspect_json(raw_json: *const c_char) -> *mut c_c
|
||||
json_result(result)
|
||||
}
|
||||
|
||||
/// 构建官方同步计划 JSON summary。
|
||||
/// Builds an official sync plan JSON summary without persisting state.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bat_sync_plan_json(
|
||||
current_json: *const c_char,
|
||||
@@ -368,16 +369,31 @@ mod tests {
|
||||
value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_version() {
|
||||
let version_ptr = bat_version();
|
||||
let version = take_string(version_ptr);
|
||||
assert!(!version.is_empty());
|
||||
fn parse_json_response(raw: String) -> serde_json::Value {
|
||||
serde_json::from_str(&raw).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manifest_inspect_json() {
|
||||
let catalog_json = r#"{
|
||||
fn inspect_manifest(raw_json: &str) -> serde_json::Value {
|
||||
let raw_json = CString::new(raw_json).unwrap();
|
||||
parse_json_response(take_string(bat_manifest_inspect_json(raw_json.as_ptr())))
|
||||
}
|
||||
|
||||
fn sync_plan(current_json: &str, previous_json: Option<&str>) -> serde_json::Value {
|
||||
let current_json = CString::new(current_json).unwrap();
|
||||
let previous_json = previous_json.map(|value| CString::new(value).unwrap());
|
||||
let previous_ptr = previous_json
|
||||
.as_ref()
|
||||
.map(|value| value.as_ptr())
|
||||
.unwrap_or(std::ptr::null());
|
||||
|
||||
parse_json_response(take_string(bat_sync_plan_json(
|
||||
current_json.as_ptr(),
|
||||
previous_ptr,
|
||||
)))
|
||||
}
|
||||
|
||||
fn catalog_json() -> &'static str {
|
||||
r#"{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIds": [
|
||||
"synthetic/minimal.bundle"
|
||||
@@ -391,34 +407,96 @@ mod tests {
|
||||
"dependencies": ["shared.bundle"]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
}"#
|
||||
}
|
||||
|
||||
let result_ptr = bat_manifest_inspect_json(CString::new(catalog_json).unwrap().as_ptr());
|
||||
let result = take_string(result_ptr);
|
||||
assert!(result.contains(r#""ok":true"#));
|
||||
assert!(result.contains(r#""resource_count":1"#));
|
||||
fn sync_snapshot_json(bundle_version: &str) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"connection_group_name":"Prod-Audit",
|
||||
"app_version":"1.70.0",
|
||||
"bundle_version":"{bundle_version}",
|
||||
"addressables_root":"https://prod-clientpatch.bluearchiveyostar.com/r93_token",
|
||||
"endpoints":[
|
||||
{{
|
||||
"kind":"TableCatalog",
|
||||
"platform":null,
|
||||
"url":"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes"
|
||||
}}
|
||||
]
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ffi_version() {
|
||||
let version_ptr = bat_version();
|
||||
let version = take_string(version_ptr);
|
||||
assert!(!version.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manifest_inspect_json() {
|
||||
let result = inspect_manifest(catalog_json());
|
||||
assert_eq!(result["ok"], true);
|
||||
assert_eq!(result["data"]["resource_count"], 1);
|
||||
assert_eq!(
|
||||
result["data"]["resources"][0]["path"],
|
||||
"synthetic/minimal.bundle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_plan_json() {
|
||||
let current = r#"{
|
||||
"connection_group_name":"Prod-Audit",
|
||||
"app_version":"1.70.0",
|
||||
"bundle_version":"s8tloc7lo3",
|
||||
"addressables_root":"https://prod-clientpatch.bluearchiveyostar.com/r93_token",
|
||||
"endpoints":[
|
||||
{
|
||||
"kind":"TableCatalog",
|
||||
"platform":null,
|
||||
"url":"https://prod-clientpatch.bluearchiveyostar.com/r93_token/TableBundles/TableCatalog.bytes"
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
let current = sync_snapshot_json("s8tloc7lo3");
|
||||
let result = sync_plan(¤t, None);
|
||||
assert_eq!(result["ok"], true);
|
||||
assert_eq!(result["data"]["decision"], "DownloadVerifyAndPublish");
|
||||
assert_eq!(result["data"]["should_download"], true);
|
||||
assert_eq!(result["data"]["should_publish"], true);
|
||||
}
|
||||
|
||||
let result_ptr =
|
||||
bat_sync_plan_json(CString::new(current).unwrap().as_ptr(), std::ptr::null());
|
||||
let result = take_string(result_ptr);
|
||||
assert!(result.contains(r#""ok":true"#));
|
||||
assert!(result.contains(r#""decision":"DownloadVerifyAndPublish""#));
|
||||
#[test]
|
||||
fn stateless_ffi_json_api_repeats_without_cached_state() {
|
||||
let first_manifest = inspect_manifest(catalog_json());
|
||||
let second_manifest = inspect_manifest(catalog_json());
|
||||
assert_eq!(first_manifest, second_manifest);
|
||||
|
||||
let current = sync_snapshot_json("s8tloc7lo3");
|
||||
let initial_plan = sync_plan(¤t, None);
|
||||
let up_to_date_plan = sync_plan(¤t, Some(¤t));
|
||||
let initial_plan_again = sync_plan(¤t, None);
|
||||
|
||||
assert_eq!(initial_plan["data"]["decision"], "DownloadVerifyAndPublish");
|
||||
assert_eq!(up_to_date_plan["data"]["decision"], "UpToDate");
|
||||
assert_eq!(
|
||||
initial_plan_again["data"]["decision"],
|
||||
initial_plan["data"]["decision"]
|
||||
);
|
||||
assert_eq!(
|
||||
initial_plan_again["data"]["should_download"],
|
||||
initial_plan["data"]["should_download"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_json_api_returns_structured_errors() {
|
||||
let manifest_error =
|
||||
parse_json_response(take_string(bat_manifest_inspect_json(std::ptr::null())));
|
||||
assert_eq!(manifest_error["ok"], false);
|
||||
assert!(manifest_error["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("null pointer"));
|
||||
|
||||
let sync_error = parse_json_response(take_string(bat_sync_plan_json(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
)));
|
||||
assert_eq!(sync_error["ok"], false);
|
||||
assert!(sync_error["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("null pointer"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user