feat: implement protocol v2 for plugin system and add comprehensive tracing support
This commit is contained in:
@@ -8,3 +8,4 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -13,12 +13,25 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ffi::{CStr, CString, c_char},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
pub const API_VERSION: u32 = 1;
|
||||
/// The newest stable wire protocol implemented by this SDK.
|
||||
pub const API_VERSION: u32 = 2;
|
||||
pub const ENGINE_VERSION: &str = "0.3.0";
|
||||
pub const MANIFEST_SYMBOL: &[u8] = b"convertis_plugin_manifest_v1\0";
|
||||
pub const FACTORY_SYMBOL: &[u8] = b"convertis_plugin_create_v1\0";
|
||||
pub const MANIFEST_SYMBOL: &[u8] = b"convertis_plugin_manifest\0";
|
||||
pub const METADATA_SYMBOL_V2: &[u8] = b"convertis_plugin_metadata_v2\0";
|
||||
pub const AVAILABILITY_SYMBOL_V2: &[u8] = b"convertis_plugin_availability_v2\0";
|
||||
pub const CONVERT_SYMBOL_V2: &[u8] = b"convertis_plugin_convert_v2\0";
|
||||
pub const FREE_SYMBOL_V2: &[u8] = b"convertis_plugin_free_string_v2\0";
|
||||
|
||||
// Protocol v1 crossed the Rust ABI boundary. These names are kept forever so
|
||||
// engines can retain an adapter for plugins built before the stable protocol.
|
||||
pub const LEGACY_MANIFEST_SYMBOL_V1: &[u8] = b"convertis_plugin_manifest_v1\0";
|
||||
pub const LEGACY_FACTORY_SYMBOL_V1: &[u8] = b"convertis_plugin_create_v1\0";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ArtifactKind {
|
||||
@@ -79,7 +92,7 @@ pub struct PluginMetadata {
|
||||
pub options: Vec<OptionSpec>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConversionRequest {
|
||||
pub input: PathBuf,
|
||||
pub output: PathBuf,
|
||||
@@ -98,19 +111,77 @@ pub trait Plugin: Send + Sync {
|
||||
|
||||
pub type PluginFactory = unsafe fn() -> Box<dyn Plugin>;
|
||||
pub type PluginManifest = unsafe extern "C" fn() -> *const std::ffi::c_char;
|
||||
pub type PluginJsonCall = unsafe extern "C" fn() -> *mut c_char;
|
||||
pub type PluginConvertCall = unsafe extern "C" fn(*const c_char) -> *mut c_char;
|
||||
pub type PluginStringFree = unsafe extern "C" fn(*mut c_char);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WireResponse<T> {
|
||||
pub ok: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<T>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> WireResponse<T> {
|
||||
pub fn success(result: T) -> Self {
|
||||
Self {
|
||||
ok: true,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure(error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
ok: false,
|
||||
result: None,
|
||||
error: Some(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn wire_json<T: Serialize>(response: WireResponse<T>) -> *mut c_char {
|
||||
let json = serde_json::to_string(&response).unwrap_or_else(|_| {
|
||||
"{\"ok\":false,\"error\":\"plugin response serialization failed\"}".to_owned()
|
||||
});
|
||||
CString::new(json)
|
||||
.expect("JSON serialization cannot contain a NUL byte")
|
||||
.into_raw()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub unsafe fn request_from_json(pointer: *const c_char) -> Result<ConversionRequest, String> {
|
||||
if pointer.is_null() {
|
||||
return Err("conversion request was null".to_owned());
|
||||
}
|
||||
let json = unsafe { CStr::from_ptr(pointer) }
|
||||
.to_str()
|
||||
.map_err(|error| format!("conversion request was not UTF-8: {error}"))?;
|
||||
serde_json::from_str(json).map_err(|error| format!("invalid conversion request: {error}"))
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub unsafe fn free_wire_string(pointer: *mut c_char) {
|
||||
if !pointer.is_null() {
|
||||
drop(unsafe { CString::from_raw(pointer) });
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! export_plugin {
|
||||
($plugin:expr, $id:literal) => {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn convertis_plugin_manifest_v1() -> *const std::ffi::c_char {
|
||||
pub extern "C" fn convertis_plugin_manifest() -> *const std::ffi::c_char {
|
||||
static MANIFEST: &str = concat!(
|
||||
"{\"api_version\":1,\"engine_version\":\"",
|
||||
"{\"manifest_version\":1,\"protocol_versions\":[2],\"engine_requirement\":\">=",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"\",\"plugin_id\":\"",
|
||||
$id,
|
||||
"\",\"rustc_version\":\"",
|
||||
env!("CONVERTIS_RUSTC_VERSION"),
|
||||
"\",\"plugin_version\":\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"\",\"target\":\"",
|
||||
env!("CONVERTIS_TARGET"),
|
||||
"\"}\0"
|
||||
@@ -119,8 +190,79 @@ macro_rules! export_plugin {
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub fn convertis_plugin_create_v1() -> Box<dyn $crate::Plugin> {
|
||||
Box::new($plugin)
|
||||
pub extern "C" fn convertis_plugin_metadata_v2() -> *mut std::ffi::c_char {
|
||||
let response = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
$crate::Plugin::metadata(&$plugin)
|
||||
}))
|
||||
.map($crate::WireResponse::success)
|
||||
.unwrap_or_else(|_| {
|
||||
$crate::WireResponse::failure("plugin panicked while returning metadata")
|
||||
});
|
||||
$crate::wire_json(response)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn convertis_plugin_availability_v2() -> *mut std::ffi::c_char {
|
||||
let response = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
$crate::Plugin::availability(&$plugin)
|
||||
}));
|
||||
let response = match response {
|
||||
Ok(Ok(())) => $crate::WireResponse::success(()),
|
||||
Ok(Err(error)) => $crate::WireResponse::failure(error),
|
||||
Err(_) => {
|
||||
$crate::WireResponse::failure("plugin panicked while checking availability")
|
||||
}
|
||||
};
|
||||
$crate::wire_json(response)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn convertis_plugin_convert_v2(
|
||||
request: *const std::ffi::c_char,
|
||||
) -> *mut std::ffi::c_char {
|
||||
let response = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
|
||||
$crate::request_from_json(request)
|
||||
.and_then(|request| $crate::Plugin::convert(&$plugin, &request))
|
||||
}));
|
||||
let response = match response {
|
||||
Ok(Ok(())) => $crate::WireResponse::success(()),
|
||||
Ok(Err(error)) => $crate::WireResponse::failure(error),
|
||||
Err(_) => $crate::WireResponse::failure("plugin panicked during conversion"),
|
||||
};
|
||||
$crate::wire_json(response)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn convertis_plugin_free_string_v2(pointer: *mut std::ffi::c_char) {
|
||||
unsafe { $crate::free_wire_string(pointer) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn conversion_requests_round_trip_through_json() {
|
||||
let request = ConversionRequest {
|
||||
input: "/tmp/input.png".into(),
|
||||
output: "/tmp/output.jpg".into(),
|
||||
from: "png".into(),
|
||||
to: "jpeg".into(),
|
||||
options: BTreeMap::from([("quality".into(), "90".into())]),
|
||||
};
|
||||
let json = CString::new(serde_json::to_string(&request).unwrap()).unwrap();
|
||||
let decoded = unsafe { request_from_json(json.as_ptr()) }.unwrap();
|
||||
assert_eq!(decoded.input, request.input);
|
||||
assert_eq!(decoded.options, request.options);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_success_is_explicit_even_though_json_result_is_null() {
|
||||
let json = serde_json::to_string(&WireResponse::success(())).unwrap();
|
||||
let decoded: WireResponse<()> = serde_json::from_str(&json).unwrap();
|
||||
assert!(decoded.ok);
|
||||
assert!(decoded.error.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user