Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ZeraTS/ironbullet/llms.txt
Use this file to discover all available pages before exploring further.
Overview
IronBullet’s plugin system uses a C-compatible FFI interface to load custom blocks from dynamic libraries (.dll files). Plugins can be written in any language that can produce C-compatible exports, though the included examples use Rust.
FFI Interface
Plugins must implement four exported functions that match the C ABI defined in src/plugin/abi.rs:
Required Exports
// Returns plugin metadata
plugin_info() -> *const PluginInfo
// Returns metadata for a specific block by index
plugin_block_info(index: u32) -> *const BlockInfo
// Executes a block with given settings and variables
plugin_execute(
block_index: u32,
settings_json: *const c_char,
variables_json: *const c_char
) -> *const ExecuteResult
// Frees strings allocated by the plugin
plugin_free_string(ptr: *const c_char)
ABI Structures
PluginInfo
Defines plugin metadata:
#[repr(C)]
pub struct PluginInfo {
pub name: *const c_char,
pub version: *const c_char,
pub author: *const c_char,
pub description: *const c_char,
pub block_count: u32,
}
Fields:
name - Plugin identifier (e.g., “ExamplePlugin”)
version - Semantic version string (e.g., “0.1.0”)
author - Author name
description - Human-readable description
block_count - Number of blocks this plugin provides
BlockInfo
Defines block metadata for UI and execution:
#[repr(C)]
pub struct BlockInfo {
pub block_type_name: *const c_char, // "PluginName.BlockName"
pub label: *const c_char, // Display name
pub category: *const c_char, // Block category
pub color: *const c_char, // Hex color code
pub icon: *const c_char, // Icon identifier
pub settings_schema_json: *const c_char,
pub default_settings_json: *const c_char,
}
Fields:
block_type_name - Unique identifier in format “PluginName.BlockName”
label - Display name shown in UI
category - Category for block grouping (e.g., “Utilities”, “Bypass”)
color - Hex color code for visual identification (e.g., “#9b59b6”)
icon - Icon identifier from icon set
settings_schema_json - JSON schema defining configurable settings
default_settings_json - JSON object with default setting values
ExecuteResult
Returned by plugin_execute to communicate execution results:
#[repr(C)]
pub struct ExecuteResult {
pub success: bool,
pub updated_variables_json: *const c_char,
pub log_message: *const c_char,
pub error_message: *const c_char,
}
Fields:
success - Whether execution completed successfully
updated_variables_json - JSON object with modified/new variables
log_message - Human-readable log message
error_message - Error description (only if success is false)
Plugin Lifecycle
1. Loading
The PluginManager scans a directory for .dll files and loads each plugin:
pub fn scan_directory(&mut self, path: &str) {
// Scans for .dll files and calls load_plugin() for each
}
For each plugin:
- Load the dynamic library using
libloading
- Call
plugin_info() to get metadata
- Call
plugin_block_info(i) for each block (0 to block_count - 1)
- Store metadata and library handle
See src/plugin/manager.rs:74-131 for the complete loading implementation.
2. Execution
When a plugin block is executed:
- Find the plugin and block by
block_type_name
- Serialize settings and variables to JSON
- Call
plugin_execute() with JSON strings
- Parse the
ExecuteResult
- Free allocated strings using
plugin_free_string()
- Return updated variables and log message
See src/plugin/manager.rs:141-206 for the execution implementation.
3. Memory Management
Critical: Plugins allocate strings that must be freed by the caller:
// Manager calls plugin_free_string() for each allocated string
free_fn(result.updated_variables_json);
free_fn(result.log_message);
free_fn(result.error_message);
Plugins typically implement plugin_free_string as:
#[no_mangle]
pub extern "C" fn plugin_free_string(ptr: *const c_char) {
if !ptr.is_null() {
unsafe {
drop(CString::from_raw(ptr as *mut c_char));
}
}
}
Hot-Loading
Plugins can be reloaded by calling scan_directory() again:
let mut manager = PluginManager::new();
manager.scan_directory("./plugins");
// Later, to reload:
manager.scan_directory("./plugins"); // Clears and reloads all plugins
Note: The current implementation clears all plugins before rescanning. The old library handles are dropped, allowing the OS to unload the DLLs if possible.
Querying Plugins
let plugins: Vec<PluginMeta> = manager.all_plugin_metas();
Returns:
pub struct PluginMeta {
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub dll_path: String,
}
let blocks: Vec<PluginBlockMeta> = manager.all_block_infos();
Returns:
pub struct PluginBlockMeta {
pub block_type_name: String,
pub label: String,
pub category: String,
pub color: String,
pub icon: String,
pub settings_schema_json: String,
pub default_settings_json: String,
pub plugin_name: String,
pub block_index: u32,
}
Executing Blocks
let result = manager.execute_block(
"ExamplePlugin.ReverseString", // block_type_name
r#"{"input_var":"data.SOURCE"}"#, // settings JSON
r#"{"data.SOURCE":"hello"}"# // variables JSON
)?;
// Returns: (success, updated_vars, log_message)
let (success, vars, log) = result;
assert_eq!(vars.get("PLUGIN_RESULT"), Some(&"olleh".to_string()));
Thread Safety
PluginManager is marked as Send + Sync:
unsafe impl Send for PluginManager {}
unsafe impl Sync for PluginManager {}
Safety considerations:
- The manager can be shared across threads
- Plugins must be thread-safe if called concurrently
- Hot-reloading requires exclusive access (mutable reference)
Error Handling
Plugins return errors via the ExecuteResult structure:
if !result.success {
let error = cstr_to_string(result.error_message);
// Free strings before returning error
free_fn(result.updated_variables_json);
free_fn(result.log_message);
free_fn(result.error_message);
return Err(error);
}
Loading errors are logged to stderr:
if let Err(e) = self.load_plugin(&p) {
eprintln!("Failed to load plugin {:?}: {}", p, e);
}
Cargo Configuration
Plugins must be compiled as dynamic libraries. In Cargo.toml:
[lib]
crate-type = ["cdylib"]
This produces:
plugin.dll on Windows
libplugin.so on Linux
libplugin.dylib on macOS
The plugin manager currently only scans for .dll files (see manager.rs:65).