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