You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Proposal-ID: ARO-0073 Author: ARO Language Team Status: Draft Created: 2026-04-11 Updated: 2026-04-11 Requires: ARO-0045 (Package Manager), ARO-0016 (Interoperability)
Summary
This proposal introduces language-native SDK libraries for Swift, Rust, C, C++, and Python that hide the raw C ABI, JSON serialization, and memory management behind idiomatic, ergonomic APIs. Plugin authors write natural code in their language of choice; the SDK generates the required aro_plugin_* exports automatically.
The proposal also replaces the three existing plugin ABIs (actions, services, qualifiers) with a single clean contract, introduces system object support, qualifier chaining and parameters, lifecycle hooks, a plugin scaffolding CLI (aro new plugin), and the Invoke mechanism for plugins to call back into ARO feature sets.
Since ARO is pre-1.0, we favor a clean API over backward compatibility. The old service ABI (aro_plugin_init + _call with out-pointers) and the separate aro_plugin_execute stub requirement for qualifier-only plugins are removed entirely. Existing example plugins will be rewritten. The Plugin Guide book will be updated to match.
Motivation
The Problem: Ceremony Over Substance
An analysis of all nine plugin examples reveals a striking imbalance between business logic and boilerplate:
Language
Logic
Ceremony
Worst Offender
C (qualifiers)
~5%
~95%
Hand-rolled JSON array parsing
Swift (actions)
~20%
~80%
NSDictionary workarounds, @_cdecl
C (actions)
~30%
~70%
Manual strstr-based JSON extraction
Swift (services)
~45%
~55%
Foundation bridging across dylib boundary
Rust
~65%
~35%
unsafe blocks for CString FFI
Python
~80%
~20%
Minimal -- but inconsistent contract
A C plugin author who wants to implement a simple "first element" qualifier must write ~150 lines of pointer arithmetic to parse a JSON array. A Swift plugin author must know about NSDictionary workarounds for cross-binary Foundation bridging. A Rust author needs six unsafe blocks for string conversions. None of this has anything to do with the plugin's actual purpose.
Specific Pain Points
No SDK or helper library. Every plugin hand-rolls JSON serialization, C ABI exports, and memory management from scratch.
Three incompatible ABI patterns with no documentation on when to use which:
C/C++ plugins must hand-roll JSON parsing. The find_json_string / extract_json_array helpers in examples are fragile -- no escape handling, fixed-size buffers, no nested object support.
Swift plugins have Foundation bridging bugs. Two separate examples document workarounds: NSDictionary instead of Dictionary for aro_plugin_info, and manual escapeJSON instead of Foundation string methods.
Qualifier-only plugins must implement a stub aro_plugin_execute that returns an error. Unnecessary ceremony.
Python uses a fundamentally different contract (per-function aro_action_*, dict return type) from native plugins (single aro_plugin_execute, JSON string return). Knowledge doesn't transfer between languages.
No type safety. All data flows through untyped JSON dictionaries with no schemas, code generation, or typed interfaces.
No event system integration for plugins. Plugins cannot subscribe to or emit domain events through a clean API.
Plugin qualifiers cannot accept parameters. Built-in qualifiers like clip and take accept arguments via the with clause, but plugin qualifiers receive only the bare value.
No qualifier chaining. There is no way to compose multiple qualifiers in a single expression.
No system object support. Plugins cannot provide custom system objects (like a Redis store) through the current ABI.
No plugin-to-runtime invocation. Plugins cannot call ARO feature sets, making hybrid plugin architectures difficult.
We replace the three existing ABIs with one clean contract. Since ARO is pre-1.0, there is no backward compatibility obligation. The old service ABI (aro_plugin_init + _call with 3-parameter out-pointer signature) is removed entirely. The AROService protocol and ServiceRegistry are deprecated and replaced by the unified plugin ABI -- the same functionality is achieved more cleanly through plugin actions and the Call action routing.
1.1 The C ABI
+--------------------------------------------------------------------+
| REQUIRED |
+--------------------------------------------------------------------+
| char* aro_plugin_info(void) |
| Returns JSON metadata describing everything the plugin provides |
+--------------------------------------------------------------------+
| void aro_plugin_free(char* ptr) |
| Frees any string returned by the plugin |
+--------------------------------------------------------------------+
| |
| OPTIONAL (based on what you provide) |
+--------------------------------------------------------------------+
| void aro_plugin_init(void) |
| One-time initialization (DB connections, model loading, etc.) |
+--------------------------------------------------------------------+
| void aro_plugin_shutdown(void) |
| Cleanup on unload (close connections, flush buffers, etc.) |
+--------------------------------------------------------------------+
| char* aro_plugin_execute(const char* action, const char* input) |
| Only needed if you provide actions or services |
+--------------------------------------------------------------------+
| char* aro_plugin_qualifier(const char* name, const char* input) |
| Only needed if you provide qualifiers |
+--------------------------------------------------------------------+
| void aro_plugin_on_event(const char* event_type, const char* data)|
| Only needed if you subscribe to events |
+--------------------------------------------------------------------+
| char* aro_object_read(const char* id, const char* qualifier) |
| Only needed if you provide system objects |
+--------------------------------------------------------------------+
| char* aro_object_write(const char* id, const char* qualifier, |
| const char* value) |
| Only needed if you provide writable system objects |
+--------------------------------------------------------------------+
| char* aro_object_list(const char* pattern) |
| Only needed if you provide enumerable system objects |
+--------------------------------------------------------------------+
| char* aro_plugin_invoke(const char* feature_set, |
| const char* input) |
| Runtime-provided: plugins call this to invoke ARO feature sets |
+--------------------------------------------------------------------+
Key design choices:
aro_plugin_info and aro_plugin_free are the only required exports
aro_plugin_execute is not required for qualifier-only or system-object-only plugins
Services route through aro_plugin_execute with action name "service:<method>"
aro_plugin_init / aro_plugin_shutdown are lifecycle hooks for stateful plugins
System objects have dedicated aro_object_read / aro_object_write / aro_object_list functions
aro_plugin_invoke is a callback provided by the runtime, enabling plugins to call ARO feature sets
Services are now declared in aro_plugin_info alongside actions and qualifiers. The runtime routes Call the <result> from the <sqlite: query> to aro_plugin_execute("service:query", input_json). No separate _call symbol needed.
1.3 What Gets Removed
The following are removed from the runtime and replaced by the clean ABI:
aro_plugin_init returning service metadata (the old service discovery pattern). Replaced by aro_plugin_info which declares everything.
3-parameter service function signature (method, args, &result -> Int32). Replaced by routing through aro_plugin_execute("service:<method>", input).
AROService protocol and ServiceRegistry. The same functionality is achieved via plugin actions and the Call action. This removes code complexity without losing any capability.
aro_plugin_execute stub requirement for qualifier-only plugins.
Existing example plugins (SQLiteExample, ZipService, GreetingPlugin, HashPluginDemo, CSVProcessor, MarkdownRenderer, all qualifier plugins) will be rewritten to use the new SDK. The Plugin Guide book chapters will be updated to match.
1.4 Input JSON: Context and Descriptors
The runtime passes rich context to plugins via the input JSON. The SDK exposes this through typed helpers:
{
"data": "the primary object value",
"object": "alias for data (backward compat)",
"qualifier": "the result qualifier (e.g., sha256)",
"preposition": "from",
"result": {
"base": "digest",
"qualifiers": ["sha256"],
"specifiers": ["sha256"]
},
"source": {
"base": "password",
"specifiers": []
},
"_context": {
"requestId": "req-abc-123",
"featureSet": "Secure Password: User Registration",
"businessActivity": "User Registration"
},
"_with": {
"encoding": "hex",
"rounds": 10
}
}
The result and source fields expose the full ResultDescriptor and ObjectDescriptor models (base, qualifiers, specifiers). The _context prefix passes execution context information. The _with field contains parameters from the with { } clause.
Different prepositions can trigger different behavior within an action. The SDK exposes the preposition and the runtime passes it in the input JSON:
@Action(verbs:["Transform"], role:.own, prepositions:[.from,.to,.into,.as])func transform(input:ActionInput)->ActionOutput{switch input.preposition {case.from:returnconvertFormat(input) // Transform <out> from <xml>
case.to:returnapplyTransform(input) // Transform <data> to <target>
case.into:returnmapToType(input) // Transform <data> into <type>
case.as:returnencode(input) // Transform <data> as <format>
default:return.error("Unsupported preposition")}}
2. Qualifier Improvements
2.1 Parameterized Qualifiers
Plugin qualifiers can now accept parameters via the with clause, just like built-in qualifiers (clip, take):
(* Plugin qualifier with parameters *)
Compute the <top-items: stats.top> from the <scores> with { count: 5 }.
Compute the <clipped: text.truncate> from the <message> with { maxLength: 100, suffix: "..." }.
The with clause parameters are passed to the qualifier function in the input JSON under the "_with" key:
Multiple qualifiers can be chained in a single expression using the pipe syntax:
(* Chain: sort the list, then take the first 3 *)
Compute the <top3: stats.sort | list.take> from the <scores> with { count: 3 }.
(* Chain: reverse, then pick a random element *)
Compute the <surprise: collections.reverse | collections.pick-random> from the <items>.
The runtime evaluates qualifiers left-to-right. Each qualifier's output becomes the next qualifier's input. Parameters from the with clause are passed to all qualifiers in the chain (each qualifier reads only the parameters it recognizes).
Implementation: The parser recognizes | within specifier positions. The QualifierRegistry receives an ordered list of qualifier names and applies them sequentially.
2.3 Qualifier Conflict Resolution
If two plugins register the same qualifier name under the same namespace, this is a load-time error. The plugin author must ensure unique qualifier names within their namespace.
If an application needs two plugins that happen to share a qualifier name, the application's manifest can alias one plugin's handle:
# In the application's aro.yaml or plugin configurationplugins:
plugin-stats-v1:
alias: StatsV1 # Override the plugin's handleplugin-stats-v2:
alias: StatsV2
This gives each plugin a distinct namespace: StatsV1.sort vs StatsV2.sort.
2.4 Ambiguity Resolution
When a qualifier name matches both a data field and a built-in operation (e.g., a field called length), the resolution order is:
Built-in operations (unqualified, e.g., length) -- if no field matches
Data field access -- if the symbol table contains a matching field
This is the existing behavior, documented here for clarity. The recommendation: always use namespaced qualifiers (handle.qualifier) to avoid ambiguity.
2.5 Unified Qualifier Registry
Built-in qualifiers (hash, length, uppercase, lowercase, clip, take, date, format, distance, intersect, difference, union) are registered in QualifierRegistry alongside plugin qualifiers. This provides a single source of truth for all available qualifiers.
The aro actions list command (see Section 9) also lists all registered qualifiers with their source (built-in vs plugin name).
3. System Objects
Plugins can provide custom system objects that integrate with ARO's Source/Sink model. System objects appear as native ARO objects:
(* Using a Redis system object provided by a plugin *)
Store the <user-data> to the <redis: users/42>.
Retrieve the <cached> from the <redis: sessions/abc>.
Log <redis: stats> to the <console>.
3.1 System Object Capabilities
Each system object declares capabilities in aro_plugin_info:
Hybrid plugins (native code + .aro files) need a way for native code to call back into ARO feature sets. The aro_plugin_invoke callback enables this:
4.1 The Invoke Callback
The runtime provides a function pointer to the plugin during initialization:
// Runtime sets this before calling any plugin functiontypedefchar* (*aro_invoke_fn)(constchar*feature_set, constchar*input_json);
voidaro_plugin_set_invoke(aro_invoke_fnfn);
Plugins call this to invoke ARO feature sets:
// From within a plugin action:char*result=aro_plugin_invoke("Validate Order: Order Validation", input_json);
// Parse result, use it in the plugin's logicaro_plugin_free(result);
4.2 SDK Wrappers
// Swift SDK
letresult=tryARORuntime.invoke("Validate Order: Order Validation", input:["order": orderData])
// Rust SDKlet result = aro_runtime::invoke("Validate Order: Order Validation",&input)?;
# Python SDKresult=aro_runtime.invoke("Validate Order: Order Validation", {"order": order_data})
This enables the hybrid plugin pattern described in Plugin Guide Chapter 14: native code handles computation (Argon2 hashing, JWT tokens), while ARO feature sets handle business logic (authentication workflows, validation rules).
5. Language SDKs
Each SDK is a thin library that:
Generates the aro_plugin_* C ABI exports
Handles JSON serialization/deserialization
Manages memory (allocation and freeing)
Provides typed input/output access including descriptors and context
Emits the aro_plugin_info response from declarations
Provides standard error codes and domain-specific error categories
Supports async operations
5.1 Swift SDK (AROPluginSDK)
Distributed as a Swift package. Plugin authors add it as a dependency.
Note: type: .dynamic is required -- without it SPM builds a static library that cannot be loaded at runtime. Simple single-file plugins (no dependencies) can also be placed as a bare .swift file in Sources/ and ARO will compile it with swiftc automatically.
The @AROPlugin macro generates all @_cdecl exports, handles the NSDictionary workaround internally, bridges async functions via Task + semaphore automatically, and manages memory with C malloc/free to avoid Foundation bridging issues.
SDK helper types:
/// Full access to plugin input data including descriptors and context
publicstructActionInput:Sendable{
// -- Data access --
publicfunc string(_ key:String)->String?
public func int(_ key:String)->Int?
public funcdouble(_ key:String)->Double?
public func bool(_ key:String)->Bool?
public func array(_ key:String)->[Any]?
public func dict(_ key:String)->[String:Any]?
// -- Descriptors --
publicvarresult:Descriptor // { base, qualifiers, specifiers }
publicvarsource:Descriptor // { base, specifiers }
publicvarpreposition:Preposition
// -- Context --
publicvarcontext:ExecutionInfo // { requestId, featureSet, businessActivity }
// -- With-clause parameters --
publicvarwith:QualifierParams}
/// Standard error codes (ARO Appendix C)
publicenumPluginErrorCode:Int{case success =0case invalidInput =1case notFound =2case permissionDenied =3case timeout =4case connectionFailed =5case executionFailed =6case invalidState =7case resourceExhausted =8case unsupported =9case rateLimited =10}
/// Domain-specific error categories
publicenumPluginErrorCategory:String{case validation // VALIDATION_MISSING_FIELD, VALIDATION_INVALID_FORMAT
case io // IO_FILE_NOT_FOUND, IO_PERMISSION_DENIED
case authentication // AUTH_INVALID_TOKEN, AUTH_EXPIRED
case rateLimiting // RATE_LIMIT_EXCEEDED, RATE_LIMIT_QUOTA
}
/// Plugin output with error support
publicenumActionOutput:Sendable{case success([String:Any])case error(PluginErrorCode,String?=nil,[String:Any]?=nil)
/// Emit an event alongside the result
func emit(_ eventType:String, data:[String:Any])-> ActionOutput
/// Invoke an ARO feature set from within the plugin
static func invoke(_ featureSet:String, input:[String:Any])throws->[String:Any]}
5.2 Rust SDK (aro-plugin-sdk)
Distributed as a crate (initially via git dependency, later via crates.io).
use aro_plugin_sdk::prelude::*;#[aro_plugin(handle = "CSV", version = "1.0.0")]mod csv_plugin {usesuper::*;#[init]fnsetup(){// One-time initialization}#[shutdown]fncleanup(){// Cleanup on unload}#[action(verbs = ["ParseCSV","ReadCSV"], role = "request", prepositions = ["from"])]fnparse_csv(input:&Input) -> Result<Output>{let data = input.string("data")?;let delimiter = input.with_params().string("delimiter").unwrap_or(",".into());let rows:Vec<Vec<String>> = data
.lines().map(|line| line.split(&*delimiter).map(String::from).collect()).collect();Ok(Output::new().set("rows",&rows).set("count", rows.len()).set("headers",&rows[0]))}#[qualifier(input_types = ["List"], accepts_parameters = true)]fntop(value:Value,params:&Params) -> Result<Value>{let count = params.int("count").unwrap_or(3)asusize;letmut arr = value.as_array()?.clone();
arr.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
arr.truncate(count);Ok(Value::Array(arr))}#[system_object(identifier = "csv-store", capabilities = ["readable","writable"])]fncsv_store(op:ObjectOp) -> Result<Output>{match op {ObjectOp::Read{ id, qualifier } => {/* ... */}ObjectOp::Write{ id, qualifier, value } => {/* ... */}
_ => Err(PluginError::unsupported("list not supported"))}}#[on_event("DataImported")]fnhandle_import(event:&EventData){let source = event.string("source").unwrap_or_default();println!("Data imported from: {source}");}}
All unsafe blocks and catch_unwind wrappers are encapsulated in aro_plugin_sdk::ffi. The developer writes zero unsafe code. The panic = "abort" profile setting prevents panics from crossing the FFI boundary.
5.3 C SDK (aro_plugin_sdk.h)
A single header file (stb-style) that plugin authors #include. No build system dependency -- just drop the header into your project. Works for both C and multi-file plugin structures.
aro_plugin_sdk.h provides:
JSON parsing helpers (using a bundled minimal JSON parser)
Memory management (arena allocator for response building)
Memory for all returned strings uses an arena allocator that is freed in bulk by aro_plugin_free. No individual malloc/free tracking needed by the plugin author.
5.4 C++ SDK (aro_plugin_sdk.hpp)
A C++ wrapper around the C SDK header, providing RAII, exception safety, and modern C++ idioms. Distributed as a header-only library (two files: aro_plugin_sdk.h + aro_plugin_sdk.hpp).
#include"aro_plugin_sdk.hpp"ARO_PLUGIN("Audio", "1.0.0");
// C++ plugins use the same macros as C, but gain RAII wrappersARO_ACTION("AnalyzeAudio", ROLE_OWN, PREP_FROM) {
// C++ exception safety: exceptions are caught at the boundary// and converted to ARO error responses automaticallyauto data = aro::input_string(ctx, "data");
auto sample_rate = aro::with_int(ctx, "sampleRate", 44100);
// RAII resource managementauto fft = std::make_unique<FFTProcessor>(sample_rate);
auto spectrum = fft->analyze(data);
// Use C++ containers freely -- the SDK serializes them
std::vector<double> peaks = spectrum.find_peaks();
aro::output_array(ctx, "peaks", peaks);
aro::output_double(ctx, "dominantFrequency", spectrum.dominant());
returnaro_ok(ctx);
}
// Qualifiers work the same wayARO_QUALIFIER("fft", "List", "Compute FFT of signal data") {
auto values = aro::qualifier_array<double>(ctx);
auto result = compute_fft(values);
returnaro::qualifier_result_array(ctx, result);
}
The C++ SDK adds:
aro:: namespace wrappers with type-safe templates
Automatic try/catch around the extern "C" boundary (all C++ exceptions are caught and converted to ARO error responses)
std::vector, std::string, std::map serialization
RAII scope guards for resource cleanup
Plugins compile with clang++ or g++ and link with -lstdc++. The SDK handles the extern "C" wrapping.
5.5 Python SDK (aro-plugin-sdk)
Distributed via pip. Plugin authors install it with pip install aro-plugin-sdk.
Plugin implementation:
fromaro_plugin_sdkimportplugin, action, qualifier, service, on_event, system_objectfromaro_plugin_sdkimportErrorCode@plugin(handle="Markdown", version="1.0.0")classMarkdownPlugin:
defon_init(self):
"""One-time initialization."""self.render_count=0defon_shutdown(self):
"""Cleanup on unload."""pass@action(verbs=["ToHTML", "RenderMarkdown"], role="own", prepositions=["from"])defto_html(self, input):
"""Convert markdown text to HTML."""importretext=input.string("data")
text=re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text=re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
self.render_count+=1return {"html": text, "renderCount": self.render_count}
@qualifier(input_types=["List"], accepts_parameters=True)defsort(self, values, params=None):
"""Sort a list of values."""reverse=params.bool("descending", default=False) ifparamselseFalsereturnsorted(values, key=str, reverse=reverse)
@system_object(identifier="render-cache", capabilities=["readable", "writable"])defrender_cache(self, operation):
ifoperation.type=="read":
return {"value": self._cache.get(operation.id)}
elifoperation.type=="write":
self._cache[operation.id] =operation.valuereturn {"stored": True}
Persistent mode -- the default for SDK-based plugins. The plugin runs as a long-lived subprocess communicating over stdin/stdout with JSON-line protocol:
fromaro_plugin_sdkimportplugin, actionimporttorch@plugin(handle="ML", version="1.0.0")classMLPlugin:
defon_init(self):
"""Detect GPU, load model with appropriate settings."""self.device="cuda"iftorch.cuda.is_available() else"cpu"ifself.device=="cuda":
fromtransformersimportBitsAndBytesConfigquantization=BitsAndBytesConfig(load_in_4bit=True)
self.model=AutoModel.from_pretrained("model", quantization_config=quantization)
else:
self.model=AutoModel.from_pretrained("model")
@action(verbs=["Embed"], role="own", prepositions=["from"])defembed(self, input):
try:
data=input.string("data")
embedding=self.model.encode(data)
return {"embedding": embedding.tolist()}
excepttorch.cuda.OutOfMemoryError:
torch.cuda.empty_cache()
returnself.error(ErrorCode.RESOURCE_EXHAUSTED, "GPU out of memory")
6. Hybrid Plugins and ARO Files
6.1 The aro-files Provider Type
Plugins can include .aro feature set files alongside native code. These are parsed by the ARO compiler and registered as feature sets within the plugin's namespace.
Event handlers: Feature sets named <EventName> Handler become event handlers
Reusable feature sets: Available via the Invoke action
Pure ARO plugins: Plugins with only aro-files providers -- no native code, no compilation
6.2 The aro-templates Provider Type
Plugins can also provide template files for the Render action:
provides:
- type: aro-templatespath: templates/
Templates are embedded during aro build and available at runtime. The scaffolding CLI supports this:
aro new plugin --name my-templates --lang aro --templates
6.3 Hybrid Loading Sequence
When a plugin has multiple providers, they load in order:
Native code (swift-plugin, rust-plugin, c-plugin, cpp-plugin) -- compiled and loaded first
ARO files (aro-files) -- parsed and registered after native code is available
Templates (aro-templates) -- registered in template registry
This ensures native actions are available when ARO feature sets reference them.
6.4 Plugin Unload/Reload
The runtime supports unloading and reloading plugins at runtime via UnifiedPluginLoader.shared.unload(pluginName:) and UnifiedPluginLoader.shared.reload(pluginName:). This is useful during development for hot-reloading plugin code without restarting the application. When a plugin is unloaded, all its actions, qualifiers, system objects, and event subscriptions are removed from their respective registries.
7. Plugin Scaffolding CLI
A new aro new plugin command generates a complete, ready-to-build plugin project.
7.1 Syntax
# Interactive mode -- prompts for language, capabilities, name
aro new plugin
# Explicit mode
aro new plugin --name my-csv-processor --lang rust --actions --qualifiers
# Quick mode -- minimal defaults
aro new plugin my-greeting --lang swift
# Pure ARO plugin (no native code)
aro new plugin my-workflows --lang aro
# With templates
aro new plugin my-email-templates --lang aro --templates
7.2 Supported Options
Flag
Description
Default
--name
Plugin name (kebab-case)
prompted
--lang
Language: swift, rust, c, cpp, python, aro
prompted
--handle
PascalCase namespace
derived from name
--actions
Include action scaffolding
yes
--qualifiers
Include qualifier scaffolding
no
--services
Include service scaffolding
no
--system-objects
Include system object scaffolding
no
--events
Include event handler scaffolding
no
--templates
Include aro-templates provider
no
--hybrid
Include both native code and aro-files
no
7.3 Generated Project Structure
Swift:
Plugins/my-greeting/
plugin.yaml
Package.swift # depends on AROPluginSDK, type: .dynamic
Sources/
MyGreetingPlugin.swift # @AROPlugin struct with example @Action
Tests/
MyGreetingTests.swift # example test
Rust:
Plugins/my-csv-processor/
plugin.yaml
Cargo.toml # depends on aro-plugin-sdk, cdylib, release profile
src/
lib.rs # #[aro_plugin] mod with example #[action]
tests/
integration.rs # example test
C:
Plugins/my-hash/
plugin.yaml
Makefile # platform-aware, auto-detects OS
include/
aro_plugin_sdk.h # the SDK header (copied in)
src/
plugin.c # ARO_PLUGIN + ARO_ACTION macros with example
C++:
Plugins/my-audio/
plugin.yaml
Makefile # C++ flags, -lstdc++
include/
aro_plugin_sdk.h # C SDK header
aro_plugin_sdk.hpp # C++ wrapper header
src/
plugin.cpp # ARO_PLUGIN + ARO_ACTION with C++ features
Python:
Plugins/my-stats/
plugin.yaml
src/
plugin.py # @plugin class with example @action
requirements.txt # includes aro-plugin-sdk
tests/
test_plugin.py # example test
Pure ARO:
Plugins/my-workflows/
plugin.yaml # provides: aro-files only
features/
example.aro # example feature set
8. aro build and Binary Embedding
When aro build compiles an ARO application to a native binary, plugins in Plugins/ are embedded directly into the binary. This is necessary because the resulting binary must be self-contained -- it should run on any machine without requiring the Plugins/ directory to be present alongside it.
The embedding process:
Each plugin's compiled library (.dylib/.so) is base64-encoded
The plugin's plugin.yaml is included alongside the encoded library
Both are stored as string constants in the LLVM IR module
At runtime startup, the binary extracts these to a temporary directory and loads them via dlopen
This means aro build ./MyApp produces a single binary that includes all plugin functionality. No separate plugin installation needed on the target machine.
9. Error Handling
9.1 Standard Error Codes
All SDKs include the standard ARO error codes (0-10):
Code
Name
Description
0
SUCCESS
Operation completed successfully
1
INVALID_INPUT
Missing or malformed input data
2
NOT_FOUND
Requested resource not found
3
PERMISSION_DENIED
Insufficient permissions
4
TIMEOUT
Operation timed out
5
CONNECTION_FAILED
Could not connect to external service
6
EXECUTION_FAILED
Internal processing error
7
INVALID_STATE
Plugin in wrong state for operation
8
RESOURCE_EXHAUSTED
Memory, disk, or GPU resources exhausted
9
UNSUPPORTED
Operation not supported
10
RATE_LIMITED
Too many requests
9.2 Domain-Specific Error Categories
Plugins can use domain-specific error codes following the naming convention {CATEGORY}_{SPECIFIC_ERROR}:
The runtime checks for these at install time and prints install commands if missing.
10.3 Deprecation Strategy
Plugins can declare deprecated features in aro_plugin_info:
{
"deprecations": [
{
"feature": "action:OldHash",
"message": "Use ComputeHash instead. OldHash will be removed in 2.0.0",
"since": "1.2.0",
"remove_in": "2.0.0"
}
]
}
The runtime emits warnings when deprecated features are used. The aro check command also reports deprecation warnings.
11. Performance
11.1 Optimization Techniques
The SDKs and documentation recommend:
Compile-time regex: Use once_cell / lazy_static (Rust) or static let (Swift) for compiled regex patterns instead of recompiling per call
Zero-copy string handling: Use std::string_view (C++), &str (Rust), or Substring (Swift) to avoid copying input data
SIMD: Use platform SIMD intrinsics for batch numerical operations (e.g., memchr crate in Rust)
Profile-guided optimization (PGO): For performance-critical plugins, use PGO with representative workloads
11.2 Rust Release Profile
The scaffolding generates an optimized release profile:
[profile.release]
lto = true# link-time optimizationcodegen-units = 1# better optimization at cost of compile timepanic = "abort"# no unwinding across FFIopt-level = "z"# optimize for size (or "3" for speed)
11.3 Python GPU Acceleration
For ML plugins, the SDK documents:
CUDA detection: torch.cuda.is_available()
Model quantization: BitsAndBytesConfig(load_in_4bit=True) for memory-constrained GPUs
OOM handling: torch.cuda.empty_cache() in error recovery
Lazy imports: Import heavy dependencies (transformers, torch) only when first needed
Model caching: Load models in on_init(), reuse across calls
12. Testing Support
Each SDK includes testing utilities so plugin authors can test without loading through the ARO runtime.
Plugins should also include .aro test files that test the plugin through the ARO runtime:
(* tests/hash-tests.aro *)
(Application-Start: Hash Tests) {
(* Test 1: Hash produces consistent results *)
Hash the <hash1: sha256> from "hello".
Hash the <hash2: sha256> from "hello".
Compare the <hash1> against the <hash2>.
When <comparison: not-equal> {
Log "FAIL: Hash not deterministic" to the <console>.
Return an <Error: status> for the <test>.
}
Log "PASS: Hash deterministic" to the <console>.
Return an <OK: status> for the <tests>.
}
Run with: aro run ./tests/hash-tests.aro
This catches issues that unit tests miss: JSON serialization bugs, registration errors, qualifier resolution failures.
12.3 Memory Safety Testing
For C/C++/Rust plugins, the documentation recommends AddressSanitizer:
# C/C++: compile with sanitizer
clang -fsanitize=address -shared -fPIC -o libplugin.dylib src/plugin.c
# Run tests through ARO
aro run ./tests/plugin-tests.aro
# Linux: Valgrind
valgrind --leak-check=full aro run ./tests/plugin-tests.aro
13. CLI Commands
The following CLI commands support the plugin development workflow:
Command
Description
aro new plugin
Scaffold a new plugin project
aro plugins list
List installed plugins (name, version, source, provides)
aro plugins list --verbose
Detailed plugin information
aro plugins validate
Check manifests, dependencies, handle conflicts
aro plugins rebuild
Recompile all native plugins
aro plugins export
Write plugin sources to .aro-sources for reproducibility
aro plugins restore
Re-install all plugins from .aro-sources
aro plugins docs <name>
Generate documentation from plugin metadata
aro actions list
List all registered actions (built-in + plugin) with source
aro check
Validate manifest, check for deprecations, verify dependencies
14. Plugin Documentation Generation
The SDK metadata enables automatic documentation generation:
aro plugins docs my-plugin # Generate markdown docs
aro plugins docs my-plugin --html # Generate HTML docs
Generated from aro_plugin_info metadata + source code docstrings. Includes: action list with verbs/role/prepositions, qualifier list with input types and parameter documentation, system objects with capabilities, event subscriptions and emissions.
Implementation Plan
Phase 1: Clean ABI and Runtime Changes
Replace the three ABIs with the unified contract
Remove AROService protocol and ServiceRegistry (route through aro_plugin_execute)
Remove old aro_plugin_init service-discovery pattern and 3-parameter _call signature
Port GreetingPlugin, QualifierPlugin, SQLiteExample, ZipService to use the SDK
Add testing utilities
Phase 6: Documentation & Polish
Add aro plugins docs command
Update all examples in the repository
Update CLAUDE.md, OVERVIEW.md, and website
Update the Plugin Guide book (all chapters)
Add aro new plugin --lang aro for pure ARO plugins and templates
Design Decisions
Why replace the old ABI instead of versioning it?
ARO is pre-1.0. Clean code is more valuable than backward compatibility at this stage. One good way is better than many legacy paths. The old service ABI (_call with out-pointers and error codes) adds complexity to the runtime without providing functionality that the unified ABI cannot achieve.
Why macros/decorators instead of code generation?
It creates generated files that must be kept in sync with the source
It adds a build step before the language's native build
Macros/decorators are idiomatic in each language and compose naturally
The generated code is invisible -- reducing cognitive overhead
Why a single-header C/C++ SDK instead of a static library?
No build system dependency -- works with any C/C++ compiler
One clean path is better than two overlapping mechanisms. All existing service-based plugins (SQLiteExample, ZipService) will be rewritten to use the new pattern.
Why add qualifier chaining?
Sequential Compute statements work but are verbose for simple transformation pipelines. Qualifier chaining with | enables:
Compute the <result: stats.sort | list.take> from the <data> with { count: 5 }.
Instead of:
Compute the <sorted: stats.sort> from the <data>.
Compute the <result: list.take> from the <sorted> with { count: 5 }.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Proposal: Plugin SDK & Developer Experience
Proposal-ID: ARO-0073
Author: ARO Language Team
Status: Draft
Created: 2026-04-11
Updated: 2026-04-11
Requires: ARO-0045 (Package Manager), ARO-0016 (Interoperability)
Summary
This proposal introduces language-native SDK libraries for Swift, Rust, C, C++, and Python that hide the raw C ABI, JSON serialization, and memory management behind idiomatic, ergonomic APIs. Plugin authors write natural code in their language of choice; the SDK generates the required
aro_plugin_*exports automatically.The proposal also replaces the three existing plugin ABIs (actions, services, qualifiers) with a single clean contract, introduces system object support, qualifier chaining and parameters, lifecycle hooks, a plugin scaffolding CLI (
aro new plugin), and the Invoke mechanism for plugins to call back into ARO feature sets.Since ARO is pre-1.0, we favor a clean API over backward compatibility. The old service ABI (
aro_plugin_init+_callwith out-pointers) and the separatearo_plugin_executestub requirement for qualifier-only plugins are removed entirely. Existing example plugins will be rewritten. The Plugin Guide book will be updated to match.Motivation
The Problem: Ceremony Over Substance
An analysis of all nine plugin examples reveals a striking imbalance between business logic and boilerplate:
@_cdeclstrstr-based JSON extractionunsafeblocks for CString FFIA C plugin author who wants to implement a simple "first element" qualifier must write ~150 lines of pointer arithmetic to parse a JSON array. A Swift plugin author must know about
NSDictionaryworkarounds for cross-binary Foundation bridging. A Rust author needs sixunsafeblocks for string conversions. None of this has anything to do with the plugin's actual purpose.Specific Pain Points
No SDK or helper library. Every plugin hand-rolls JSON serialization, C ABI exports, and memory management from scratch.
Three incompatible ABI patterns with no documentation on when to use which:
aro_plugin_execute(action, input_json) -> result_jsonaro_plugin_init()+service_call(method, args, &result) -> Int32aro_plugin_qualifier(name, input_json) -> result_jsonC/C++ plugins must hand-roll JSON parsing. The
find_json_string/extract_json_arrayhelpers in examples are fragile -- no escape handling, fixed-size buffers, no nested object support.Swift plugins have Foundation bridging bugs. Two separate examples document workarounds:
NSDictionaryinstead ofDictionaryforaro_plugin_info, and manualescapeJSONinstead of Foundation string methods.Qualifier-only plugins must implement a stub
aro_plugin_executethat returns an error. Unnecessary ceremony.Python uses a fundamentally different contract (per-function
aro_action_*, dict return type) from native plugins (singlearo_plugin_execute, JSON string return). Knowledge doesn't transfer between languages.No type safety. All data flows through untyped JSON dictionaries with no schemas, code generation, or typed interfaces.
No event system integration for plugins. Plugins cannot subscribe to or emit domain events through a clean API.
Plugin qualifiers cannot accept parameters. Built-in qualifiers like
clipandtakeaccept arguments via thewithclause, but plugin qualifiers receive only the bare value.No qualifier chaining. There is no way to compose multiple qualifiers in a single expression.
No system object support. Plugins cannot provide custom system objects (like a Redis store) through the current ABI.
No plugin-to-runtime invocation. Plugins cannot call ARO feature sets, making hybrid plugin architectures difficult.
The Vision
A Swift plugin author should be able to write:
A Rust plugin author should be able to write:
A C or C++ plugin author should be able to write:
A Python plugin author should be able to write:
Proposed Solution
1. Clean Plugin ABI
We replace the three existing ABIs with one clean contract. Since ARO is pre-1.0, there is no backward compatibility obligation. The old service ABI (
aro_plugin_init+_callwith 3-parameter out-pointer signature) is removed entirely. TheAROServiceprotocol andServiceRegistryare deprecated and replaced by the unified plugin ABI -- the same functionality is achieved more cleanly through plugin actions and theCallaction routing.1.1 The C ABI
Key design choices:
aro_plugin_infoandaro_plugin_freeare the only required exportsaro_plugin_executeis not required for qualifier-only or system-object-only pluginsaro_plugin_executewith action name"service:<method>"aro_plugin_init/aro_plugin_shutdownare lifecycle hooks for stateful pluginsaro_object_read/aro_object_write/aro_object_listfunctionsaro_plugin_invokeis a callback provided by the runtime, enabling plugins to call ARO feature sets1.2 Unified Info JSON Schema
{ "name": "plugin-name", "version": "1.0.0", "actions": [ { "name": "ComputeHash", "verbs": ["hash", "computehash"], "role": "own", "prepositions": ["from", "with"], "description": "Computes a hash of the input data" } ], "services": [ { "name": "sqlite", "methods": ["query", "execute", "connect", "disconnect"], "description": "SQLite database service" } ], "qualifiers": [ { "name": "reverse", "input_types": ["List", "String"], "accepts_parameters": true, "description": "Reverses the order of elements" } ], "system_objects": [ { "identifier": "redis", "capabilities": ["readable", "writable", "enumerable", "watchable"], "description": "Redis key-value store" } ], "events": { "emits": ["DataProcessed", "CacheInvalidated"], "subscribes": ["UserCreated", "OrderPlaced"] }, "deprecations": [ { "feature": "action:OldHash", "message": "Use ComputeHash instead", "since": "1.2.0", "remove_in": "2.0.0" } ] }Services are now declared in
aro_plugin_infoalongside actions and qualifiers. The runtime routesCall the <result> from the <sqlite: query>toaro_plugin_execute("service:query", input_json). No separate_callsymbol needed.1.3 What Gets Removed
The following are removed from the runtime and replaced by the clean ABI:
aro_plugin_initreturning service metadata (the old service discovery pattern). Replaced byaro_plugin_infowhich declares everything.method, args, &result -> Int32). Replaced by routing througharo_plugin_execute("service:<method>", input).AROServiceprotocol andServiceRegistry. The same functionality is achieved via plugin actions and theCallaction. This removes code complexity without losing any capability.aro_plugin_executestub requirement for qualifier-only plugins.Existing example plugins (
SQLiteExample,ZipService,GreetingPlugin,HashPluginDemo,CSVProcessor,MarkdownRenderer, all qualifier plugins) will be rewritten to use the new SDK. The Plugin Guide book chapters will be updated to match.1.4 Input JSON: Context and Descriptors
The runtime passes rich context to plugins via the input JSON. The SDK exposes this through typed helpers:
{ "data": "the primary object value", "object": "alias for data (backward compat)", "qualifier": "the result qualifier (e.g., sha256)", "preposition": "from", "result": { "base": "digest", "qualifiers": ["sha256"], "specifiers": ["sha256"] }, "source": { "base": "password", "specifiers": [] }, "_context": { "requestId": "req-abc-123", "featureSet": "Secure Password: User Registration", "businessActivity": "User Registration" }, "_with": { "encoding": "hex", "rounds": 10 } }The
resultandsourcefields expose the full ResultDescriptor and ObjectDescriptor models (base, qualifiers, specifiers). The_contextprefix passes execution context information. The_withfield contains parameters from thewith { }clause.SDK helpers provide typed access to all of this:
1.5 Preposition-Based Dispatch
Different prepositions can trigger different behavior within an action. The SDK exposes the preposition and the runtime passes it in the input JSON:
2. Qualifier Improvements
2.1 Parameterized Qualifiers
Plugin qualifiers can now accept parameters via the
withclause, just like built-in qualifiers (clip,take):The
withclause parameters are passed to the qualifier function in the input JSON under the"_with"key:{ "value": [95, 87, 72, 100, 63, 91], "type": "List", "_with": { "count": 5 } }SDK qualifier declarations indicate parameter support:
2.2 Qualifier Chaining (Composition)
Multiple qualifiers can be chained in a single expression using the pipe syntax:
The runtime evaluates qualifiers left-to-right. Each qualifier's output becomes the next qualifier's input. Parameters from the
withclause are passed to all qualifiers in the chain (each qualifier reads only the parameters it recognizes).Implementation: The parser recognizes
|within specifier positions. TheQualifierRegistryreceives an ordered list of qualifier names and applies them sequentially.2.3 Qualifier Conflict Resolution
If two plugins register the same qualifier name under the same namespace, this is a load-time error. The plugin author must ensure unique qualifier names within their namespace.
If an application needs two plugins that happen to share a qualifier name, the application's manifest can alias one plugin's handle:
This gives each plugin a distinct namespace:
StatsV1.sortvsStatsV2.sort.2.4 Ambiguity Resolution
When a qualifier name matches both a data field and a built-in operation (e.g., a field called
length), the resolution order is:collections.length) -- always unambiguouslength) -- if no field matchesThis is the existing behavior, documented here for clarity. The recommendation: always use namespaced qualifiers (
handle.qualifier) to avoid ambiguity.2.5 Unified Qualifier Registry
Built-in qualifiers (
hash,length,uppercase,lowercase,clip,take,date,format,distance,intersect,difference,union) are registered inQualifierRegistryalongside plugin qualifiers. This provides a single source of truth for all available qualifiers.The
aro actions listcommand (see Section 9) also lists all registered qualifiers with their source (built-in vs plugin name).3. System Objects
Plugins can provide custom system objects that integrate with ARO's Source/Sink model. System objects appear as native ARO objects:
3.1 System Object Capabilities
Each system object declares capabilities in
aro_plugin_info:readablearo_object_read(id, qualifier)Retrieve the <x> from the <redis: key>writablearo_object_write(id, qualifier, value)Store the <x> to the <redis: key>enumerablearo_object_list(pattern)Retrieve the <keys> from the <redis: *>watchablearo_plugin_on_event3.2 SDK Support
4. Plugin-to-Runtime Invocation
Hybrid plugins (native code +
.arofiles) need a way for native code to call back into ARO feature sets. Thearo_plugin_invokecallback enables this:4.1 The Invoke Callback
The runtime provides a function pointer to the plugin during initialization:
Plugins call this to invoke ARO feature sets:
4.2 SDK Wrappers
This enables the hybrid plugin pattern described in Plugin Guide Chapter 14: native code handles computation (Argon2 hashing, JWT tokens), while ARO feature sets handle business logic (authentication workflows, validation rules).
5. Language SDKs
Each SDK is a thin library that:
aro_plugin_*C ABI exportsaro_plugin_inforesponse from declarations5.1 Swift SDK (
AROPluginSDK)Distributed as a Swift package. Plugin authors add it as a dependency.
Package.swift for a plugin:
Note:
type: .dynamicis required -- without it SPM builds a static library that cannot be loaded at runtime. Simple single-file plugins (no dependencies) can also be placed as a bare.swiftfile inSources/and ARO will compile it withswiftcautomatically.Plugin implementation:
The
@AROPluginmacro generates all@_cdeclexports, handles theNSDictionaryworkaround internally, bridgesasyncfunctions viaTask+ semaphore automatically, and manages memory with Cmalloc/freeto avoid Foundation bridging issues.SDK helper types:
5.2 Rust SDK (
aro-plugin-sdk)Distributed as a crate (initially via git dependency, later via crates.io).
Cargo.toml:
Plugin implementation:
All
unsafeblocks andcatch_unwindwrappers are encapsulated inaro_plugin_sdk::ffi. The developer writes zero unsafe code. Thepanic = "abort"profile setting prevents panics from crossing the FFI boundary.5.3 C SDK (
aro_plugin_sdk.h)A single header file (stb-style) that plugin authors
#include. No build system dependency -- just drop the header into your project. Works for both C and multi-file plugin structures.aro_plugin_sdk.h provides:
Plugin implementation:
The
aro_ctxhelpers:Memory for all returned strings uses an arena allocator that is freed in bulk by
aro_plugin_free. No individualmalloc/freetracking needed by the plugin author.5.4 C++ SDK (
aro_plugin_sdk.hpp)A C++ wrapper around the C SDK header, providing RAII, exception safety, and modern C++ idioms. Distributed as a header-only library (two files:
aro_plugin_sdk.h+aro_plugin_sdk.hpp).The C++ SDK adds:
aro::namespace wrappers with type-safe templatestry/catcharound theextern "C"boundary (all C++ exceptions are caught and converted to ARO error responses)std::vector,std::string,std::mapserializationPlugins compile with
clang++org++and link with-lstdc++. The SDK handles theextern "C"wrapping.5.5 Python SDK (
aro-plugin-sdk)Distributed via pip. Plugin authors install it with
pip install aro-plugin-sdk.Plugin implementation:
Persistent mode -- the default for SDK-based plugins. The plugin runs as a long-lived subprocess communicating over stdin/stdout with JSON-line protocol:
GPU acceleration support for ML plugins:
6. Hybrid Plugins and ARO Files
6.1 The
aro-filesProvider TypePlugins can include
.arofeature set files alongside native code. These are parsed by the ARO compiler and registered as feature sets within the plugin's namespace.The
aro-filesprovider enables:<EventName> Handlerbecome event handlersInvokeactionaro-filesproviders -- no native code, no compilation6.2 The
aro-templatesProvider TypePlugins can also provide template files for the
Renderaction:Templates are embedded during
aro buildand available at runtime. The scaffolding CLI supports this:6.3 Hybrid Loading Sequence
When a plugin has multiple providers, they load in order:
This ensures native actions are available when ARO feature sets reference them.
6.4 Plugin Unload/Reload
The runtime supports unloading and reloading plugins at runtime via
UnifiedPluginLoader.shared.unload(pluginName:)andUnifiedPluginLoader.shared.reload(pluginName:). This is useful during development for hot-reloading plugin code without restarting the application. When a plugin is unloaded, all its actions, qualifiers, system objects, and event subscriptions are removed from their respective registries.7. Plugin Scaffolding CLI
A new
aro new plugincommand generates a complete, ready-to-build plugin project.7.1 Syntax
7.2 Supported Options
--name--langswift,rust,c,cpp,python,aro--handle--actions--qualifiers--services--system-objects--events--templates--hybrid7.3 Generated Project Structure
Swift:
Rust:
C:
C++:
Python:
Pure ARO:
8.
aro buildand Binary EmbeddingWhen
aro buildcompiles an ARO application to a native binary, plugins inPlugins/are embedded directly into the binary. This is necessary because the resulting binary must be self-contained -- it should run on any machine without requiring thePlugins/directory to be present alongside it.The embedding process:
.dylib/.so) is base64-encodedplugin.yamlis included alongside the encoded librarydlopenThis means
aro build ./MyAppproduces a single binary that includes all plugin functionality. No separate plugin installation needed on the target machine.9. Error Handling
9.1 Standard Error Codes
All SDKs include the standard ARO error codes (0-10):
SUCCESSINVALID_INPUTNOT_FOUNDPERMISSION_DENIEDTIMEOUTCONNECTION_FAILEDEXECUTION_FAILEDINVALID_STATERESOURCE_EXHAUSTEDUNSUPPORTEDRATE_LIMITED9.2 Domain-Specific Error Categories
Plugins can use domain-specific error codes following the naming convention
{CATEGORY}_{SPECIFIC_ERROR}:VALIDATIONVALIDATION_MISSING_FIELD,VALIDATION_INVALID_FORMAT,VALIDATION_OUT_OF_RANGEIOIO_FILE_NOT_FOUND,IO_PERMISSION_DENIED,IO_DISK_FULLAUTHAUTH_INVALID_TOKEN,AUTH_EXPIRED,AUTH_INSUFFICIENT_SCOPERATE_LIMITRATE_LIMIT_EXCEEDED,RATE_LIMIT_QUOTA_EXHAUSTED9.3 Error Message Convention
Plugin error messages are appended to the ARO statement context by the runtime:
Error messages should be concise and specific -- they are shown directly to the user.
10. Manifest Additions
10.1 Platform-Specific Configuration
Plugins can declare platform requirements:
Platform-specific build overrides within providers:
10.2 System Requirements
The
requirements.systemfield documents system library dependencies:The runtime checks for these at install time and prints install commands if missing.
10.3 Deprecation Strategy
Plugins can declare deprecated features in
aro_plugin_info:{ "deprecations": [ { "feature": "action:OldHash", "message": "Use ComputeHash instead. OldHash will be removed in 2.0.0", "since": "1.2.0", "remove_in": "2.0.0" } ] }The runtime emits warnings when deprecated features are used. The
aro checkcommand also reports deprecation warnings.11. Performance
11.1 Optimization Techniques
The SDKs and documentation recommend:
once_cell/lazy_static(Rust) orstatic let(Swift) for compiled regex patterns instead of recompiling per callstd::string_view(C++),&str(Rust), orSubstring(Swift) to avoid copying input datamemchrcrate in Rust)11.2 Rust Release Profile
The scaffolding generates an optimized release profile:
11.3 Python GPU Acceleration
For ML plugins, the SDK documents:
torch.cuda.is_available()BitsAndBytesConfig(load_in_4bit=True)for memory-constrained GPUstorch.cuda.empty_cache()in error recoverytransformers,torch) only when first neededon_init(), reuse across calls12. Testing Support
Each SDK includes testing utilities so plugin authors can test without loading through the ARO runtime.
12.1 Unit Testing (Per-Language)
Swift:
Rust:
C:
Python:
12.2 Component Testing with ARO Files
Plugins should also include
.arotest files that test the plugin through the ARO runtime:Run with:
aro run ./tests/hash-tests.aroThis catches issues that unit tests miss: JSON serialization bugs, registration errors, qualifier resolution failures.
12.3 Memory Safety Testing
For C/C++/Rust plugins, the documentation recommends AddressSanitizer:
13. CLI Commands
The following CLI commands support the plugin development workflow:
aro new pluginaro plugins listaro plugins list --verbosearo plugins validatearo plugins rebuildaro plugins export.aro-sourcesfor reproducibilityaro plugins restore.aro-sourcesaro plugins docs <name>aro actions listaro check14. Plugin Documentation Generation
The SDK metadata enables automatic documentation generation:
Generated from
aro_plugin_infometadata + source code docstrings. Includes: action list with verbs/role/prepositions, qualifier list with input types and parameter documentation, system objects with capabilities, event subscriptions and emissions.Implementation Plan
Phase 1: Clean ABI and Runtime Changes
AROServiceprotocol andServiceRegistry(route througharo_plugin_execute)aro_plugin_initservice-discovery pattern and 3-parameter_callsignaturearo_plugin_init()/aro_plugin_shutdown()lifecycle hooksaro_plugin_on_eventsupport toNativePluginHostandPythonPluginHost_eventsresponse parsing to plugin action wrappersaro_object_read/write/list)aro_plugin_invokecallback mechanismQualifierRegistry(single source of truth)_within qualifier input JSON)_contextin plugin input JSONPhase 2: Python SDK + Persistent Mode
aro-plugin-sdkPython package with decorators and helpersPythonPluginHostaro new plugin --lang pythonscaffoldingQualifierPluginPythonandMarkdownRendererexamples to use the SDKPhase 3: C/C++ SDK (Header-Only)
aro_plugin_sdk.hsingle-header library with JSON parser and arena allocatoraro_plugin_sdk.hppC++ wrapper with RAII, exception safety, templatesARO_PLUGIN,ARO_ACTION,ARO_QUALIFIER,ARO_SYSTEM_OBJECT, etc.aro new plugin --lang cand--lang cppscaffoldingHashPluginDemo,QualifierPluginCexamples to use the SDKPhase 4: Rust SDK (Proc Macro Crate)
aro-plugin-sdkcrate with proc macros and FFI helpers#[aro_plugin],#[action],#[qualifier],#[system_object]macrosaro new plugin --lang rustscaffoldingCSVProcessorexample to use the SDKPhase 5: Swift SDK (Swift Macros)
AROPluginSDKSwift package with macros and helpers@AROPlugin,@Action,@Qualifier,@Service,@SystemObject,@OnEvent,@OnInit,@OnShutdownmacrosaro new plugin --lang swiftscaffoldingGreetingPlugin,QualifierPlugin,SQLiteExample,ZipServiceto use the SDKPhase 6: Documentation & Polish
aro plugins docscommandaro new plugin --lang arofor pure ARO plugins and templatesDesign Decisions
Why replace the old ABI instead of versioning it?
ARO is pre-1.0. Clean code is more valuable than backward compatibility at this stage. One good way is better than many legacy paths. The old service ABI (
_callwith out-pointers and error codes) adds complexity to the runtime without providing functionality that the unified ABI cannot achieve.Why macros/decorators instead of code generation?
Why a single-header C/C++ SDK instead of a static library?
Why persistent mode for Python instead of embedding?
Why deprecate AROService / ServiceRegistry?
The
AROServiceprotocol (init(),call(),shutdown()) is an older pattern that duplicates functionality now covered by the unified plugin ABI:init()->aro_plugin_init()call()->aro_plugin_execute("service:<method>", ...)shutdown()->aro_plugin_shutdown()One clean path is better than two overlapping mechanisms. All existing service-based plugins (SQLiteExample, ZipService) will be rewritten to use the new pattern.
Why add qualifier chaining?
Sequential
Computestatements work but are verbose for simple transformation pipelines. Qualifier chaining with|enables:Instead of:
Appendix A: SDK Comparison Matrix
Appendix B: Ceremony Reduction Estimates
Appendix C: Affected Code and Books
The following must be updated when this proposal is implemented:
Runtime code to modify:
Sources/ARORuntime/Plugins/UnifiedPluginLoader.swift-- new ABI loadingSources/ARORuntime/Plugins/NativePluginHost.swift-- lifecycle hooks, system objects, invoke callbackSources/ARORuntime/Plugins/PythonPluginHost.swift-- persistent mode, lifecycle hooksSources/ARORuntime/Services/PluginLoader.swift-- remove legacy service ABISources/ARORuntime/Actions/ActionRegistry.swift-- qualifier chaining supportSources/ARORuntime/Qualifiers/QualifierRegistry.swift-- built-in registration, parameters, conflictsSources/AROParser/Parser.swift-- pipe syntax for qualifier chainingRuntime code to remove:
AROServiceprotocol andServiceRegistry(replaced by unified plugin ABI)aro_plugin_initservice-discovery code path_callfunction loading inNativePluginHostExamples to rewrite:
Examples/GreetingPlugin/-- Swift SDKExamples/HashPluginDemo/-- C SDKExamples/CSVProcessor/-- Rust SDKExamples/MarkdownRenderer/-- Python SDKExamples/SQLiteExample/-- Swift SDK (service pattern -> action pattern)Examples/ZipService/-- Swift SDK (service pattern -> action pattern)Examples/QualifierPlugin/-- Swift SDKExamples/QualifierPluginC/-- C SDKExamples/QualifierPluginPython/-- Python SDKBook chapters to update:
Book/ThePluginGuide/-- All chapters reflecting new ABI, SDK usage, removed service patternBook/TheLanguageGuide/Chapter24-CustomActions.mdBook/TheLanguageGuide/Chapter25-CustomServices.md-- rewrite for unified approachBook/TheLanguageGuide/Chapter26-Plugins.mdCanonical source:
Proposals/ARO-0073-plugin-sdk.mdonmain. Edits should be made there.All reactions