feat: 进程内动态库插件系统

This commit is contained in:
lanyeeee
2026-03-11 08:57:43 +08:00
parent 6be3289d93
commit 933f8000dd
26 changed files with 2011 additions and 52 deletions
+3
View File
@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
target/
+215
View File
@@ -0,0 +1,215 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bilibili-video-downloader-plugin-api"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "bilibili-video-downloader-plugin-sdk"
version = "0.1.0"
dependencies = [
"bilibili-video-downloader-plugin-api",
"eyre",
"parking_lot",
"serde_json",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+3
View File
@@ -0,0 +1,3 @@
[workspace]
members = ["plugin-api", "plugin-sdk"]
resolver = "3"
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "bilibili-video-downloader-plugin-api"
version = "0.1.0"
edition = "2024"
[lib]
name = "bilibili_video_downloader_plugin_api"
[dependencies]
serde = { version = "1", features = ["derive"] }
+349
View File
@@ -0,0 +1,349 @@
pub const SDK_API_VERSION_V1: u32 = 1;
pub mod v1 {
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HookPointV1 {
AfterPrepare,
BeforeVideoProcess,
OnCompleted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginFailurePolicy {
FailOpen,
FailClosed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginDescriptorV1 {
pub sdk_api_version: u32,
pub id: String,
pub name: String,
pub version: String,
pub hooks: Vec<HookPointV1>,
pub failure_policy: PluginFailurePolicy,
pub description: String,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookReadonlyMetaV1 {
pub app_version: String,
pub os: String,
pub arch: String,
pub process_id: u32,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BeforeVideoProcessPayloadV1 {
pub progress: DownloadProgressV1,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AfterPreparePayloadV1 {
pub progress: DownloadProgressV1,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OnCompletedPayloadV1 {
pub progress: DownloadProgressV1,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum HookPayloadV1 {
BeforeVideoProcess(BeforeVideoProcessPayloadV1),
AfterPrepare(AfterPreparePayloadV1),
OnCompleted(OnCompletedPayloadV1),
}
impl Default for HookPayloadV1 {
fn default() -> Self {
Self::BeforeVideoProcess(BeforeVideoProcessPayloadV1::default())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HookInputV1 {
pub hook_point: HookPointV1,
pub payload: HookPayloadV1,
pub readonly_meta: HookReadonlyMetaV1,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HookOutputV1 {
pub payload: HookPayloadV1,
}
pub type HostApiGetConfigJsonV1 =
unsafe extern "C" fn(out_ptr: *mut *mut u8, out_len: *mut usize) -> i32;
pub type HostApiFreeBufferV1 = unsafe extern "C" fn(ptr: *mut u8, len: usize);
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct HostApiV1 {
pub get_config_json: HostApiGetConfigJsonV1,
pub free_buffer: HostApiFreeBufferV1,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProxyModeV1 {
#[default]
NoProxy,
System,
Custom,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileExistActionV1 {
#[default]
Overwrite,
Skip,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct CanvasConfigV1 {
pub duration: f64,
pub width: u32,
pub height: u32,
pub font: String,
pub font_size: u32,
pub width_ratio: f64,
pub horizontal_gap: f64,
pub lane_size: u32,
pub float_percentage: f64,
pub alpha: f64,
pub bold: bool,
pub outline: f64,
pub time_offset: f64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
#[allow(clippy::struct_field_names)]
pub struct HostConfigV1 {
pub download_dir: PathBuf,
pub enable_file_logger: bool,
pub sessdata: String,
pub video_quality_priority: Vec<VideoQualityV1>,
pub codec_type_priority: Vec<CodecTypeV1>,
pub audio_quality_priority: Vec<AudioQualityV1>,
pub download_video: bool,
pub download_audio: bool,
pub auto_merge: bool,
pub embed_chapter: bool,
pub embed_skip: bool,
pub download_xml_danmaku: bool,
pub download_ass_danmaku: bool,
pub download_json_danmaku: bool,
pub download_subtitle: bool,
pub download_cover: bool,
pub download_nfo: bool,
pub download_json: bool,
pub dir_fmt: String,
pub dir_fmt_for_part: String,
pub time_fmt: String,
pub proxy_mode: ProxyModeV1,
pub proxy_host: String,
pub proxy_port: u16,
pub task_concurrency: usize,
pub task_download_interval_sec: u64,
pub chunk_concurrency: usize,
pub chunk_download_interval_sec: u64,
pub danmaku_config: CanvasConfigV1,
pub file_exist_action: FileExistActionV1,
pub auto_start_download_task: bool,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EpisodeTypeV1 {
#[default]
Normal,
Bangumi,
Cheese,
}
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[repr(i64)]
pub enum VideoQualityV1 {
#[default]
Unknown = -1,
#[serde(rename = "240P")]
Video240P = 6,
#[serde(rename = "360P")]
Video360P = 16,
#[serde(rename = "480P")]
Video480P = 32,
#[serde(rename = "720P")]
Video720P = 64,
#[serde(rename = "720P60")]
Video720P60 = 74,
#[serde(rename = "1080P")]
Video1080P = 80,
#[serde(rename = "AiRepair")]
VideoAiRepair = 100,
#[serde(rename = "1080P+")]
Video1080PPlus = 112,
#[serde(rename = "1080P60")]
Video1080P60 = 116,
#[serde(rename = "4K")]
Video4K = 120,
#[serde(rename = "HDR")]
VideoHDR = 125,
#[serde(rename = "Dolby")]
VideoDolby = 126,
#[serde(rename = "8K")]
Video8K = 127,
}
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[repr(i64)]
pub enum AudioQualityV1 {
#[default]
Unknown = -1,
#[serde(rename = "64K")]
Audio64K = 30216,
#[serde(rename = "132K")]
Audio132K = 30232,
#[serde(rename = "192K")]
Audio192K = 30280,
#[serde(rename = "Dolby")]
AudioDolby = 30250,
#[serde(rename = "HiRes")]
AudioHiRes = 30251,
}
#[derive(Default, Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[repr(i64)]
pub enum CodecTypeV1 {
#[default]
Unknown = -1,
Audio = 0,
AVC = 7,
HEVC = 12,
AV1 = 13,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaChunkV1 {
pub start: u64,
pub end: u64,
pub completed: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct VideoTaskV1 {
pub selected: bool,
pub url: String,
pub video_quality: VideoQualityV1,
pub codec_type: CodecTypeV1,
pub content_length: u64,
pub chunks: Vec<MediaChunkV1>,
pub completed: bool,
pub skipped: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioTaskV1 {
pub selected: bool,
pub url: String,
pub audio_quality: AudioQualityV1,
pub content_length: u64,
pub chunks: Vec<MediaChunkV1>,
pub completed: bool,
pub skipped: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
pub struct VideoProcessTaskV1 {
pub merge_selected: bool,
pub embed_chapter_selected: bool,
pub embed_skip_selected: bool,
pub completed: bool,
pub skipped: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct SubtitleTaskV1 {
pub selected: bool,
pub completed: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
pub struct DanmakuTaskV1 {
pub xml_selected: bool,
pub ass_selected: bool,
pub json_selected: bool,
pub completed: bool,
pub skipped: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct CoverTaskV1 {
pub selected: bool,
pub url: String,
pub completed: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct NfoTaskV1 {
pub selected: bool,
pub completed: bool,
pub skipped: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct JsonTaskV1 {
pub selected: bool,
pub completed: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct DownloadProgressV1 {
pub task_id: String,
pub episode_type: EpisodeTypeV1,
pub aid: i64,
pub bvid: Option<String>,
pub cid: i64,
pub ep_id: Option<i64>,
pub duration: u64,
pub pub_ts: i64,
pub collection_title: String,
pub part_title: Option<String>,
pub part_order: Option<i64>,
pub episode_title: String,
pub episode_order: i64,
pub up_name: Option<String>,
pub up_uid: Option<i64>,
pub up_avatar: Option<String>,
pub episode_dir: PathBuf,
pub filename: String,
pub video_task: VideoTaskV1,
pub audio_task: AudioTaskV1,
pub video_process_task: VideoProcessTaskV1,
pub subtitle_task: SubtitleTaskV1,
pub danmaku_task: DanmakuTaskV1,
pub cover_task: CoverTaskV1,
pub nfo_task: NfoTaskV1,
pub json_task: JsonTaskV1,
pub create_ts: u64,
pub completed_ts: Option<u64>,
pub is_drm: bool,
pub is_preview: bool,
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "bilibili-video-downloader-plugin-sdk"
version = "0.1.0"
edition = "2024"
[lib]
name = "bilibili_video_downloader_plugin_sdk"
[dependencies]
bilibili-video-downloader-plugin-api = { path = "../plugin-api" }
eyre = { version = "0.6.12" }
serde_json = { version = "1" }
parking_lot = { version = "0.12.5" }
+191
View File
@@ -0,0 +1,191 @@
pub use bilibili_video_downloader_plugin_api::SDK_API_VERSION_V1 as SDK_API_VERSION;
pub use bilibili_video_downloader_plugin_api::v1::{
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, CanvasConfigV1, DownloadProgressV1,
FileExistActionV1, HookInputV1, HookOutputV1, HookPayloadV1, HookPointV1, HostApiV1,
HostConfigV1, OnCompletedPayloadV1, PluginDescriptorV1, PluginFailurePolicy, ProxyModeV1,
};
pub use eyre;
pub use parking_lot;
pub use serde_json;
use std::sync::LazyLock;
use parking_lot::Mutex;
pub trait PluginV1: Default + Send + 'static {
fn descriptor(&self) -> PluginDescriptorV1;
#[allow(clippy::missing_errors_doc)]
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1>;
}
static HOST_API_V1: LazyLock<Mutex<Option<HostApiV1>>> = LazyLock::new(|| Mutex::new(None));
#[doc(hidden)]
pub unsafe fn register_host_api_v1(api_ptr: *const HostApiV1) -> i32 {
if api_ptr.is_null() {
return 1;
}
let api = unsafe { *api_ptr };
*HOST_API_V1.lock() = Some(api);
0
}
fn get_host_api_v1() -> eyre::Result<HostApiV1> {
HOST_API_V1
.lock()
.as_ref()
.copied()
.ok_or_else(|| eyre::eyre!("host api 未注册"))
}
pub mod host {
use crate::HostConfigV1;
#[allow(clippy::missing_errors_doc)]
pub fn get_config() -> eyre::Result<HostConfigV1> {
let host_api = crate::get_host_api_v1()?;
let mut output_ptr: *mut u8 = std::ptr::null_mut();
let mut output_len: usize = 0;
let rc = unsafe { (host_api.get_config_json)(&raw mut output_ptr, &raw mut output_len) };
if rc != 0 {
return Err(eyre::eyre!("host get_config_json 调用失败: rc={rc}"));
}
if output_ptr.is_null() {
return Err(eyre::eyre!("host get_config_json 返回的缓冲区为空指针"));
}
let output_bytes = unsafe { std::slice::from_raw_parts(output_ptr, output_len) }.to_vec();
unsafe {
(host_api.free_buffer)(output_ptr, output_len);
}
let host_config = serde_json::from_slice::<HostConfigV1>(&output_bytes)?;
Ok(host_config)
}
}
#[macro_export]
macro_rules! export_plugin_v1 {
($ty:ty) => {
use std::ffi::{CString, c_char};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::LazyLock;
use $crate::parking_lot::Mutex;
fn to_cstring_lossy(value: String) -> CString {
// Replacing NUL guarantees CString invariants and avoids fallible construction.
let sanitized = value.replace('\0', " ");
unsafe { CString::from_vec_unchecked(sanitized.into_bytes()) }
}
static INSTANCE_V1: LazyLock<Mutex<$ty>> = LazyLock::new(|| Mutex::new(<$ty>::default()));
static DESCRIPTOR_JSON_V1: LazyLock<CString> = LazyLock::new(|| {
let instance = INSTANCE_V1.lock();
let descriptor = instance.descriptor();
let descriptor_json = match $crate::serde_json::to_string(&descriptor) {
Ok(json) => json,
Err(err) => format!("{{\"error\":\"序列化 descriptor 失败: {err}\"}}"),
};
to_cstring_lossy(descriptor_json)
});
static LAST_ERROR_V1: LazyLock<Mutex<CString>> =
LazyLock::new(|| Mutex::new(to_cstring_lossy(String::new())));
fn set_last_error_v1(message: String) {
let mut guard = LAST_ERROR_V1.lock();
*guard = to_cstring_lossy(message);
}
#[unsafe(export_name = "bilibili_video_downloader_plugin_descriptor_v1")]
pub extern "C" fn descriptor_v1() -> *const c_char {
DESCRIPTOR_JSON_V1.as_ptr()
}
#[unsafe(export_name = "bilibili_video_downloader_plugin_last_error_v1")]
pub extern "C" fn last_error_v1() -> *const c_char {
LAST_ERROR_V1.lock().as_ptr()
}
#[unsafe(export_name = "bilibili_video_downloader_plugin_set_host_api_v1")]
pub unsafe extern "C" fn set_host_api_v1(api: *const $crate::HostApiV1) -> i32 {
let rc = unsafe { $crate::register_host_api_v1(api) };
if rc != 0 {
set_last_error_v1("无效的 host api 指针".to_string());
}
rc
}
#[unsafe(export_name = "bilibili_video_downloader_plugin_on_hook_v1")]
pub unsafe extern "C" fn on_hook_v1(
input_ptr: *const u8,
input_len: usize,
out_ptr: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
if input_ptr.is_null() || out_ptr.is_null() || out_len.is_null() {
set_last_error_v1("参数里有空指针".to_string());
return 1;
}
let input_slice = unsafe { std::slice::from_raw_parts(input_ptr, input_len) };
let hook_input: $crate::HookInputV1 = match $crate::serde_json::from_slice(input_slice)
{
Ok(input) => input,
Err(err) => {
set_last_error_v1(format!("解析 hook 输入失败: {err}"));
return 2;
}
};
let hook_output = match catch_unwind(AssertUnwindSafe(|| {
let mut plugin = INSTANCE_V1.lock();
plugin.on_hook(hook_input)
})) {
Ok(Ok(output)) => output,
Ok(Err(err)) => {
set_last_error_v1(format!("{err:?}"));
return 3;
}
Err(_) => {
set_last_error_v1("处理 on_hook 时插件内部发生 panic".to_string());
return 5;
}
};
let output_bytes = match $crate::serde_json::to_vec(&hook_output) {
Ok(bytes) => bytes,
Err(err) => {
set_last_error_v1(format!("序列化 hook 输出失败: {err}"));
return 4;
}
};
let boxed = output_bytes.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *mut u8;
unsafe {
*out_ptr = ptr;
*out_len = len;
}
0
}
#[unsafe(export_name = "bilibili_video_downloader_plugin_free_buffer_v1")]
pub unsafe extern "C" fn free_buffer_v1(ptr: *mut u8, len: usize) {
if ptr.is_null() || len == 0 {
return;
}
let raw_slice = std::ptr::slice_from_raw_parts_mut(ptr, len);
unsafe {
drop(Box::from_raw(raw_slice));
}
}
};
}
@@ -0,0 +1,54 @@
use bilibili_video_downloader_plugin_sdk::{
AfterPreparePayloadV1, BeforeVideoProcessPayloadV1, HookInputV1, HookOutputV1, HookPayloadV1,
HookPointV1, OnCompletedPayloadV1, PluginDescriptorV1, PluginFailurePolicy, PluginV1,
SDK_API_VERSION, export_plugin_v1, eyre, host,
};
#[derive(Default)]
struct MacroSmokePlugin;
impl PluginV1 for MacroSmokePlugin {
fn descriptor(&self) -> PluginDescriptorV1 {
PluginDescriptorV1 {
sdk_api_version: SDK_API_VERSION,
id: "macro-smoke".to_string(),
name: "Macro Smoke".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
hooks: vec![HookPointV1::BeforeVideoProcess],
failure_policy: PluginFailurePolicy::FailOpen,
description: "Compile-time macro smoke test".to_string(),
}
}
fn on_hook(&mut self, input: HookInputV1) -> eyre::Result<HookOutputV1> {
let payload = match input.payload {
HookPayloadV1::BeforeVideoProcess(payload) => {
HookPayloadV1::BeforeVideoProcess(BeforeVideoProcessPayloadV1 {
progress: payload.progress,
})
}
HookPayloadV1::AfterPrepare(payload) => {
HookPayloadV1::AfterPrepare(AfterPreparePayloadV1 {
progress: payload.progress,
})
}
HookPayloadV1::OnCompleted(payload) => {
HookPayloadV1::OnCompleted(OnCompletedPayloadV1 {
progress: payload.progress,
})
}
};
Ok(HookOutputV1 { payload })
}
}
export_plugin_v1!(MacroSmokePlugin);
#[test]
fn macro_smoke_builds() {
assert_eq!(SDK_API_VERSION, 1);
let err = host::get_config().unwrap_err();
assert!(err.to_string().contains("host api 未注册"));
}