1use serde::{Deserialize, Serialize};
11use std::os::raw::{c_char, c_int};
12
13pub const SDK_ABI_VERSION: u32 = 1;
16
17#[repr(C)]
19pub struct PluginManifest {
20 pub abi_version: u32,
22 pub name: *const c_char,
24 pub version: *const c_char,
26 pub description: *const c_char,
28 pub vtable: PluginVtable,
30}
31
32unsafe impl Send for PluginManifest {}
34unsafe impl Sync for PluginManifest {}
35
36#[repr(C)]
39pub struct PluginVtable {
40 pub introspect:
47 unsafe extern "C" fn(input_json: *const c_char, out_json: *mut *mut c_char) -> c_int,
48
49 pub free_string: unsafe extern "C" fn(ptr: *mut c_char),
51}
52
53#[derive(Debug, Serialize, Deserialize)]
55pub struct IntrospectResponse {
56 pub schema: crate::schema::Schema,
57}
58
59#[macro_export]
76macro_rules! declare_plugin {
77 (
78 name: $name:expr,
79 version: $version:expr,
80 description: $desc:expr,
81 introspect: $introspect:path $(,)?
82 ) => {
83 const _APPCTL_PLUGIN_NAME: &[u8] = concat!($name, "\0").as_bytes();
84 const _APPCTL_PLUGIN_VERSION: &[u8] = concat!($version, "\0").as_bytes();
85 const _APPCTL_PLUGIN_DESC: &[u8] = concat!($desc, "\0").as_bytes();
86
87 #[unsafe(no_mangle)]
88 pub unsafe extern "C" fn appctl_plugin_register() -> *const $crate::ffi::PluginManifest {
89 static MANIFEST: $crate::ffi::PluginManifest = $crate::ffi::PluginManifest {
90 abi_version: $crate::ffi::SDK_ABI_VERSION,
91 name: _APPCTL_PLUGIN_NAME.as_ptr() as *const ::std::os::raw::c_char,
92 version: _APPCTL_PLUGIN_VERSION.as_ptr() as *const ::std::os::raw::c_char,
93 description: _APPCTL_PLUGIN_DESC.as_ptr() as *const ::std::os::raw::c_char,
94 vtable: $crate::ffi::PluginVtable {
95 introspect: _appctl_plugin_introspect_cabi,
96 free_string: _appctl_plugin_free_string_cabi,
97 },
98 };
99 &MANIFEST as *const _
100 }
101
102 unsafe extern "C" fn _appctl_plugin_introspect_cabi(
103 input_json: *const ::std::os::raw::c_char,
104 out_json: *mut *mut ::std::os::raw::c_char,
105 ) -> ::std::os::raw::c_int {
106 let input_str = if input_json.is_null() {
107 "{}".to_string()
108 } else {
109 unsafe { ::std::ffi::CStr::from_ptr(input_json) }
110 .to_string_lossy()
111 .into_owned()
112 };
113
114 let res: ::std::result::Result<$crate::schema::Schema, ::anyhow::Error> = (|| {
115 let input: $crate::SyncInput = ::serde_json::from_str(&input_str)?;
116 let rt = ::tokio::runtime::Builder::new_current_thread()
117 .enable_all()
118 .build()?;
119 rt.block_on($introspect(input))
120 })();
121
122 let (code, payload) = match res {
123 Ok(schema) => {
124 let env = $crate::ffi::IntrospectResponse { schema };
125 match ::serde_json::to_string(&env) {
126 Ok(json) => (0, json),
127 Err(err) => (1, format!("{{\"error\":\"serialize failed: {}\"}}", err)),
128 }
129 }
130 Err(err) => (
131 1,
132 format!(
133 "{{\"error\":{}}}",
134 ::serde_json::to_string(&err.to_string())
135 .unwrap_or_else(|_| "\"unknown\"".into())
136 ),
137 ),
138 };
139
140 match ::std::ffi::CString::new(payload) {
141 Ok(cstr) => unsafe {
142 *out_json = cstr.into_raw();
143 code
144 },
145 Err(_) => unsafe {
146 *out_json = ::std::ptr::null_mut();
147 2
148 },
149 }
150 }
151
152 unsafe extern "C" fn _appctl_plugin_free_string_cabi(ptr: *mut ::std::os::raw::c_char) {
153 if !ptr.is_null() {
154 unsafe {
155 drop(::std::ffi::CString::from_raw(ptr));
156 }
157 }
158 }
159 };
160}
161
162pub unsafe fn cstr_to_string(ptr: *const c_char) -> Option<String> {
169 if ptr.is_null() {
170 None
171 } else {
172 Some(
173 unsafe { std::ffi::CStr::from_ptr(ptr) }
174 .to_string_lossy()
175 .into_owned(),
176 )
177 }
178}