Skip to main content

binoc_sdk/
plugin_abi.rs

1//! C-ABI stable protocol for native plugins.
2//!
3//! Plugins compiled as separate cdylibs expose a set of `#[no_mangle] extern "C"`
4//! functions. The host loads them via `libloading` and calls them with JSON-serialized
5//! requests/responses, avoiding any Rust ABI compatibility requirements.
6//!
7//! Plugin authors never use this module directly — the [`crate::export_plugin!`] macro
8//! generates the entry points, and the host's native plugin loader consumes them.
9
10use serde::{Deserialize, Serialize};
11
12use crate::ir::DiffNode;
13use crate::traits::{ComparatorDescriptor, RendererDescriptor, TransformerDescriptor};
14use crate::types::{CompareResult, ItemPair, TransformResult};
15
16// ── Plugin description ─────────────────────────────────────────────
17
18/// Top-level plugin description returned by `_binoc_plugin_describe`.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PluginDescription {
21    pub sdk_version: String,
22    #[serde(default)]
23    pub comparators: Vec<ComparatorDescriptor>,
24    #[serde(default)]
25    pub transformers: Vec<TransformerDescriptor>,
26    #[serde(default)]
27    pub renderers: Vec<RendererDescriptor>,
28}
29
30// ── Comparator wire types ──────────────────────────────────────────
31
32#[derive(Debug, Serialize, Deserialize)]
33pub struct CompareRequest {
34    pub pair: ItemPair,
35    pub data_root: String,
36    pub workspace: String,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40#[serde(tag = "status")]
41pub enum CompareResponse {
42    #[serde(rename = "ok")]
43    Ok { result: Box<CompareResult> },
44    #[serde(rename = "error")]
45    Error { message: String },
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49pub struct ReopenRequest {
50    pub pair: ItemPair,
51    pub child_path: String,
52    pub data_root: String,
53    pub workspace: String,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57#[serde(tag = "status")]
58pub enum ReopenResponse {
59    #[serde(rename = "ok")]
60    Ok { pair: Box<ItemPair> },
61    #[serde(rename = "error")]
62    Error { message: String },
63}
64
65// ── Transformer wire types ─────────────────────────────────────────
66
67#[derive(Debug, Serialize, Deserialize)]
68pub struct TransformRequest {
69    pub node: DiffNode,
70    pub data_root: String,
71    #[serde(default)]
72    pub config: serde_json::Value,
73}
74
75/// Serializable version of TransformResult for the C ABI.
76#[derive(Debug, Serialize, Deserialize)]
77#[serde(tag = "status")]
78pub enum TransformResponse {
79    #[serde(rename = "unchanged")]
80    Unchanged,
81    #[serde(rename = "replace")]
82    Replace { node: Box<DiffNode> },
83    #[serde(rename = "replace_many")]
84    ReplaceMany { nodes: Vec<DiffNode> },
85    #[serde(rename = "remove")]
86    Remove,
87    #[serde(rename = "error")]
88    Error { message: String },
89}
90
91impl TransformResponse {
92    pub fn into_result(self) -> Result<TransformResult, String> {
93        match self {
94            Self::Unchanged => Ok(TransformResult::Unchanged),
95            Self::Replace { node } => Ok(TransformResult::Replace(node)),
96            Self::ReplaceMany { nodes } => Ok(TransformResult::ReplaceMany(nodes)),
97            Self::Remove => Ok(TransformResult::Remove),
98            Self::Error { message } => Err(message),
99        }
100    }
101}
102
103// ── Renderer wire types ───────────────────────────────────────────
104
105#[derive(Debug, Serialize, Deserialize)]
106pub struct RenderRequest {
107    pub changesets: Vec<crate::ir::Changeset>,
108    pub config: serde_json::Value,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
112#[serde(tag = "status")]
113pub enum RenderResponse {
114    #[serde(rename = "ok")]
115    Ok { output: String },
116    #[serde(rename = "error")]
117    Error { message: String },
118}
119
120// ── Extract wire types ─────────────────────────────────────────────
121
122#[derive(Debug, Serialize, Deserialize)]
123pub struct ExtractRequest {
124    pub node: DiffNode,
125    pub aspect: String,
126    pub data_root: String,
127}
128
129#[derive(Debug, Serialize, Deserialize)]
130#[serde(tag = "status")]
131pub enum ExtractResponse {
132    #[serde(rename = "text")]
133    Text { content: String },
134    #[serde(rename = "binary")]
135    Binary { content: Vec<u8> },
136    #[serde(rename = "none")]
137    None,
138    #[serde(rename = "error")]
139    Error { message: String },
140}
141
142// ── export_plugin! macro ───────────────────────────────────────────
143
144/// Export a plugin pack with any combination of comparators, transformers,
145/// and renderers.
146///
147/// Generates C ABI entry points conditionally based on declared types:
148/// - `_binoc_plugin_describe` (always)
149/// - `_binoc_free_string` (always)
150/// - `_binoc_comparator_compare`, `_binoc_comparator_reopen`,
151///   `_binoc_comparator_extract` (if comparators declared)
152/// - `_binoc_transformer_transform`, `_binoc_transformer_extract`
153///   (if transformers declared)
154/// - `_binoc_renderer_render` (if renderers declared)
155/// - Empty `#[pymodule]` when `python` feature active
156///
157/// # Example
158///
159/// ```ignore
160/// export_plugin! {
161///     module: my_plugin,
162///     comparators: [MyComparator],
163///     transformers: [MyTransformer],
164/// }
165/// ```
166#[macro_export]
167macro_rules! export_plugin {
168    // Internal: collect comparator descriptors
169    (@comp_descs $($comp:ty),*) => {{
170        let mut descs = Vec::new();
171        $(
172            descs.push($crate::Comparator::descriptor(
173                &<$comp as ::std::default::Default>::default(),
174            ));
175        )*
176        descs
177    }};
178
179    // Internal: collect transformer descriptors
180    (@trans_descs $($trans:ty),*) => {{
181        let mut descs = Vec::new();
182        $(
183            descs.push($crate::Transformer::descriptor(
184                &<$trans as ::std::default::Default>::default(),
185            ));
186        )*
187        descs
188    }};
189
190    // Internal: collect renderer descriptors
191    (@out_descs $($out:ty),*) => {{
192        let mut descs = Vec::new();
193        $(
194            descs.push($crate::Renderer::descriptor(
195                &<$out as ::std::default::Default>::default(),
196            ));
197        )*
198        descs
199    }};
200
201    // Internal: comparator entry points
202    (@comparator_fns $($comp:ty),+) => {
203        #[no_mangle]
204        pub unsafe extern "C" fn _binoc_comparator_compare(
205            index: u32,
206            request: *const ::std::ffi::c_char,
207        ) -> *mut ::std::ffi::c_char {
208            let response = ::std::panic::catch_unwind(|| {
209                let request_str = ::std::ffi::CStr::from_ptr(request)
210                    .to_str()
211                    .expect("binoc SDK: valid UTF-8 request");
212                let req: $crate::plugin_abi::CompareRequest =
213                    $crate::_reexport::serde_json::from_str(request_str)
214                        .expect("binoc SDK: deserialize CompareRequest");
215                let data = $crate::LocalDataAccess::for_plugin(
216                    ::std::path::PathBuf::from(&req.data_root),
217                    ::std::path::PathBuf::from(&req.workspace),
218                );
219                let comparators: Vec<Box<dyn $crate::Comparator>> =
220                    vec![$(Box::new(<$comp as ::std::default::Default>::default())),+];
221                let comp = &comparators[index as usize];
222                match $crate::Comparator::compare(comp.as_ref(), &req.pair, &data) {
223                    Ok(result) => $crate::plugin_abi::CompareResponse::Ok {
224                        result: Box::new(result),
225                    },
226                    Err(e) => $crate::plugin_abi::CompareResponse::Error {
227                        message: e.to_string(),
228                    },
229                }
230            });
231            let response = match response {
232                Ok(r) => r,
233                Err(_) => $crate::plugin_abi::CompareResponse::Error {
234                    message: "plugin panicked".to_string(),
235                },
236            };
237            let json = $crate::_reexport::serde_json::to_string(&response)
238                .expect("binoc SDK: serialize compare response");
239            ::std::ffi::CString::new(json)
240                .expect("binoc SDK: CString from JSON")
241                .into_raw()
242        }
243
244        #[no_mangle]
245        pub unsafe extern "C" fn _binoc_comparator_reopen(
246            index: u32,
247            request: *const ::std::ffi::c_char,
248        ) -> *mut ::std::ffi::c_char {
249            let response = ::std::panic::catch_unwind(|| {
250                let request_str = ::std::ffi::CStr::from_ptr(request)
251                    .to_str()
252                    .expect("binoc SDK: valid UTF-8 request");
253                let req: $crate::plugin_abi::ReopenRequest =
254                    $crate::_reexport::serde_json::from_str(request_str)
255                        .expect("binoc SDK: deserialize ReopenRequest");
256                let data = $crate::LocalDataAccess::for_plugin(
257                    ::std::path::PathBuf::from(&req.data_root),
258                    ::std::path::PathBuf::from(&req.workspace),
259                );
260                let comparators: Vec<Box<dyn $crate::Comparator>> =
261                    vec![$(Box::new(<$comp as ::std::default::Default>::default())),+];
262                let comp = &comparators[index as usize];
263                match $crate::Comparator::reopen(comp.as_ref(), &req.pair, &req.child_path, &data) {
264                    Ok(pair) => $crate::plugin_abi::ReopenResponse::Ok {
265                        pair: ::std::boxed::Box::new(pair),
266                    },
267                    Err(e) => $crate::plugin_abi::ReopenResponse::Error {
268                        message: e.to_string(),
269                    },
270                }
271            });
272            let response = match response {
273                Ok(r) => r,
274                Err(_) => $crate::plugin_abi::ReopenResponse::Error {
275                    message: "plugin panicked".to_string(),
276                },
277            };
278            let json = $crate::_reexport::serde_json::to_string(&response)
279                .expect("binoc SDK: serialize reopen response");
280            ::std::ffi::CString::new(json)
281                .expect("binoc SDK: CString from JSON")
282                .into_raw()
283        }
284
285        #[no_mangle]
286        pub unsafe extern "C" fn _binoc_comparator_extract(
287            index: u32,
288            request: *const ::std::ffi::c_char,
289        ) -> *mut ::std::ffi::c_char {
290            let response = ::std::panic::catch_unwind(|| {
291                let request_str = ::std::ffi::CStr::from_ptr(request)
292                    .to_str()
293                    .expect("binoc SDK: valid UTF-8 request");
294                let req: $crate::plugin_abi::ExtractRequest =
295                    $crate::_reexport::serde_json::from_str(request_str)
296                        .expect("binoc SDK: deserialize ExtractRequest");
297                let data = $crate::LocalDataAccess::with_data_root(
298                    ::std::path::PathBuf::from(&req.data_root),
299                );
300                let comparators: Vec<Box<dyn $crate::Comparator>> =
301                    vec![$(Box::new(<$comp as ::std::default::Default>::default())),+];
302                let comp = &comparators[index as usize];
303                match $crate::Comparator::extract(comp.as_ref(), &req.node, &req.aspect, &data) {
304                    Some($crate::ExtractResult::Text(t)) => {
305                        $crate::plugin_abi::ExtractResponse::Text { content: t }
306                    }
307                    Some($crate::ExtractResult::Binary(b)) => {
308                        $crate::plugin_abi::ExtractResponse::Binary { content: b }
309                    }
310                    None => $crate::plugin_abi::ExtractResponse::None,
311                }
312            });
313            let response = match response {
314                Ok(r) => r,
315                Err(_) => $crate::plugin_abi::ExtractResponse::Error {
316                    message: "plugin panicked".to_string(),
317                },
318            };
319            let json = $crate::_reexport::serde_json::to_string(&response)
320                .expect("binoc SDK: serialize extract response");
321            ::std::ffi::CString::new(json)
322                .expect("binoc SDK: CString from JSON")
323                .into_raw()
324        }
325    };
326
327    // Internal: transformer entry points
328    (@transformer_fns $($trans:ty),+) => {
329        #[no_mangle]
330        pub unsafe extern "C" fn _binoc_transformer_transform(
331            index: u32,
332            request: *const ::std::ffi::c_char,
333        ) -> *mut ::std::ffi::c_char {
334            let response = ::std::panic::catch_unwind(|| {
335                let request_str = ::std::ffi::CStr::from_ptr(request)
336                    .to_str()
337                    .expect("binoc SDK: valid UTF-8 request");
338                let req: $crate::plugin_abi::TransformRequest =
339                    $crate::_reexport::serde_json::from_str(request_str)
340                        .expect("binoc SDK: deserialize TransformRequest");
341                let data = $crate::LocalDataAccess::with_data_root(
342                    ::std::path::PathBuf::from(&req.data_root),
343                );
344                let transformers: Vec<Box<dyn $crate::Transformer>> =
345                    vec![$(Box::new(<$trans as ::std::default::Default>::default())),+];
346                let trans = &transformers[index as usize];
347                match $crate::Transformer::transform(trans.as_ref(), req.node, &data, &req.config) {
348                    $crate::TransformResult::Unchanged => {
349                        $crate::plugin_abi::TransformResponse::Unchanged
350                    }
351                    $crate::TransformResult::Replace(node) => {
352                        $crate::plugin_abi::TransformResponse::Replace { node }
353                    }
354                    $crate::TransformResult::ReplaceMany(nodes) => {
355                        $crate::plugin_abi::TransformResponse::ReplaceMany { nodes }
356                    }
357                    $crate::TransformResult::Remove => {
358                        $crate::plugin_abi::TransformResponse::Remove
359                    }
360                    _ => $crate::plugin_abi::TransformResponse::Unchanged,
361                }
362            });
363            let response = match response {
364                Ok(r) => r,
365                Err(_) => $crate::plugin_abi::TransformResponse::Error {
366                    message: "plugin panicked".to_string(),
367                },
368            };
369            let json = $crate::_reexport::serde_json::to_string(&response)
370                .expect("binoc SDK: serialize transform response");
371            ::std::ffi::CString::new(json)
372                .expect("binoc SDK: CString from JSON")
373                .into_raw()
374        }
375
376        #[no_mangle]
377        pub unsafe extern "C" fn _binoc_transformer_extract(
378            index: u32,
379            request: *const ::std::ffi::c_char,
380        ) -> *mut ::std::ffi::c_char {
381            let response = ::std::panic::catch_unwind(|| {
382                let request_str = ::std::ffi::CStr::from_ptr(request)
383                    .to_str()
384                    .expect("binoc SDK: valid UTF-8 request");
385                let req: $crate::plugin_abi::ExtractRequest =
386                    $crate::_reexport::serde_json::from_str(request_str)
387                        .expect("binoc SDK: deserialize ExtractRequest");
388                let data = $crate::LocalDataAccess::with_data_root(
389                    ::std::path::PathBuf::from(&req.data_root),
390                );
391                let transformers: Vec<Box<dyn $crate::Transformer>> =
392                    vec![$(Box::new(<$trans as ::std::default::Default>::default())),+];
393                let trans = &transformers[index as usize];
394                match $crate::Transformer::extract(trans.as_ref(), &req.node, &req.aspect, &data) {
395                    Some($crate::ExtractResult::Text(t)) => {
396                        $crate::plugin_abi::ExtractResponse::Text { content: t }
397                    }
398                    Some($crate::ExtractResult::Binary(b)) => {
399                        $crate::plugin_abi::ExtractResponse::Binary { content: b }
400                    }
401                    None => $crate::plugin_abi::ExtractResponse::None,
402                }
403            });
404            let response = match response {
405                Ok(r) => r,
406                Err(_) => $crate::plugin_abi::ExtractResponse::Error {
407                    message: "plugin panicked".to_string(),
408                },
409            };
410            let json = $crate::_reexport::serde_json::to_string(&response)
411                .expect("binoc SDK: serialize extract response");
412            ::std::ffi::CString::new(json)
413                .expect("binoc SDK: CString from JSON")
414                .into_raw()
415        }
416    };
417
418    // Internal: renderer entry points
419    (@renderer_fns $($out:ty),+) => {
420        #[no_mangle]
421        pub unsafe extern "C" fn _binoc_renderer_render(
422            index: u32,
423            request: *const ::std::ffi::c_char,
424        ) -> *mut ::std::ffi::c_char {
425            let response = ::std::panic::catch_unwind(|| {
426                let request_str = ::std::ffi::CStr::from_ptr(request)
427                    .to_str()
428                    .expect("binoc SDK: valid UTF-8 request");
429                let req: $crate::plugin_abi::RenderRequest =
430                    $crate::_reexport::serde_json::from_str(request_str)
431                        .expect("binoc SDK: deserialize RenderRequest");
432                let renderers: Vec<Box<dyn $crate::Renderer>> =
433                    vec![$(Box::new(<$out as ::std::default::Default>::default())),+];
434                let out = &renderers[index as usize];
435                match $crate::Renderer::render(out.as_ref(), &req.changesets, &req.config) {
436                    Ok(output) => $crate::plugin_abi::RenderResponse::Ok { output },
437                    Err(e) => $crate::plugin_abi::RenderResponse::Error {
438                        message: e.to_string(),
439                    },
440                }
441            });
442            let response = match response {
443                Ok(r) => r,
444                Err(_) => $crate::plugin_abi::RenderResponse::Error {
445                    message: "plugin panicked".to_string(),
446                },
447            };
448            let json = $crate::_reexport::serde_json::to_string(&response)
449                .expect("binoc SDK: serialize render response");
450            ::std::ffi::CString::new(json)
451                .expect("binoc SDK: CString from JSON")
452                .into_raw()
453        }
454    };
455
456    // ── Public entry: comparators only ─────────────────────────────
457    (
458        module: $module_name:ident,
459        comparators: [$($comp:ty),+ $(,)?] $(,)?
460    ) => {
461        #[no_mangle]
462        pub extern "C" fn _binoc_plugin_describe() -> *mut ::std::ffi::c_char {
463            let desc = $crate::plugin_abi::PluginDescription {
464                sdk_version: $crate::SDK_VERSION.to_string(),
465                comparators: $crate::export_plugin!(@comp_descs $($comp),+),
466                transformers: vec![],
467                renderers: vec![],
468            };
469            let json = $crate::_reexport::serde_json::to_string(&desc)
470                .expect("binoc SDK: serialize plugin description");
471            ::std::ffi::CString::new(json)
472                .expect("binoc SDK: CString from JSON")
473                .into_raw()
474        }
475
476        #[no_mangle]
477        pub unsafe extern "C" fn _binoc_free_string(s: *mut ::std::ffi::c_char) {
478            if !s.is_null() {
479                drop(::std::ffi::CString::from_raw(s));
480            }
481        }
482
483        $crate::export_plugin!(@comparator_fns $($comp),+);
484
485        #[cfg(feature = "python")]
486        #[::pyo3::pymodule]
487        fn $module_name(_m: &::pyo3::Bound<'_, ::pyo3::types::PyModule>) -> ::pyo3::PyResult<()> {
488            Ok(())
489        }
490    };
491
492    // ── Public entry: transformers only ────────────────────────────
493    (
494        module: $module_name:ident,
495        transformers: [$($trans:ty),+ $(,)?] $(,)?
496    ) => {
497        #[no_mangle]
498        pub extern "C" fn _binoc_plugin_describe() -> *mut ::std::ffi::c_char {
499            let desc = $crate::plugin_abi::PluginDescription {
500                sdk_version: $crate::SDK_VERSION.to_string(),
501                comparators: vec![],
502                transformers: $crate::export_plugin!(@trans_descs $($trans),+),
503                renderers: vec![],
504            };
505            let json = $crate::_reexport::serde_json::to_string(&desc)
506                .expect("binoc SDK: serialize plugin description");
507            ::std::ffi::CString::new(json)
508                .expect("binoc SDK: CString from JSON")
509                .into_raw()
510        }
511
512        #[no_mangle]
513        pub unsafe extern "C" fn _binoc_free_string(s: *mut ::std::ffi::c_char) {
514            if !s.is_null() {
515                drop(::std::ffi::CString::from_raw(s));
516            }
517        }
518
519        $crate::export_plugin!(@transformer_fns $($trans),+);
520
521        #[cfg(feature = "python")]
522        #[::pyo3::pymodule]
523        fn $module_name(_m: &::pyo3::Bound<'_, ::pyo3::types::PyModule>) -> ::pyo3::PyResult<()> {
524            Ok(())
525        }
526    };
527
528    // ── Public entry: renderers only ──────────────────────────────
529    (
530        module: $module_name:ident,
531        renderers: [$($out:ty),+ $(,)?] $(,)?
532    ) => {
533        #[no_mangle]
534        pub extern "C" fn _binoc_plugin_describe() -> *mut ::std::ffi::c_char {
535            let desc = $crate::plugin_abi::PluginDescription {
536                sdk_version: $crate::SDK_VERSION.to_string(),
537                comparators: vec![],
538                transformers: vec![],
539                renderers: $crate::export_plugin!(@out_descs $($out),+),
540            };
541            let json = $crate::_reexport::serde_json::to_string(&desc)
542                .expect("binoc SDK: serialize plugin description");
543            ::std::ffi::CString::new(json)
544                .expect("binoc SDK: CString from JSON")
545                .into_raw()
546        }
547
548        #[no_mangle]
549        pub unsafe extern "C" fn _binoc_free_string(s: *mut ::std::ffi::c_char) {
550            if !s.is_null() {
551                drop(::std::ffi::CString::from_raw(s));
552            }
553        }
554
555        $crate::export_plugin!(@renderer_fns $($out),+);
556
557        #[cfg(feature = "python")]
558        #[::pyo3::pymodule]
559        fn $module_name(_m: &::pyo3::Bound<'_, ::pyo3::types::PyModule>) -> ::pyo3::PyResult<()> {
560            Ok(())
561        }
562    };
563
564    // ── Public entry: comparators + transformers ───────────────────
565    (
566        module: $module_name:ident,
567        comparators: [$($comp:ty),+ $(,)?],
568        transformers: [$($trans:ty),+ $(,)?] $(,)?
569    ) => {
570        #[no_mangle]
571        pub extern "C" fn _binoc_plugin_describe() -> *mut ::std::ffi::c_char {
572            let desc = $crate::plugin_abi::PluginDescription {
573                sdk_version: $crate::SDK_VERSION.to_string(),
574                comparators: $crate::export_plugin!(@comp_descs $($comp),+),
575                transformers: $crate::export_plugin!(@trans_descs $($trans),+),
576                renderers: vec![],
577            };
578            let json = $crate::_reexport::serde_json::to_string(&desc)
579                .expect("binoc SDK: serialize plugin description");
580            ::std::ffi::CString::new(json)
581                .expect("binoc SDK: CString from JSON")
582                .into_raw()
583        }
584
585        #[no_mangle]
586        pub unsafe extern "C" fn _binoc_free_string(s: *mut ::std::ffi::c_char) {
587            if !s.is_null() {
588                drop(::std::ffi::CString::from_raw(s));
589            }
590        }
591
592        $crate::export_plugin!(@comparator_fns $($comp),+);
593        $crate::export_plugin!(@transformer_fns $($trans),+);
594
595        #[cfg(feature = "python")]
596        #[::pyo3::pymodule]
597        fn $module_name(_m: &::pyo3::Bound<'_, ::pyo3::types::PyModule>) -> ::pyo3::PyResult<()> {
598            Ok(())
599        }
600    };
601}