From 705249019cdc1884ae81c8ea0f70e9a2d9a7b200 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Thu, 9 Oct 2025 05:18:35 -0700 Subject: [PATCH] feat: Implement job registration system for WASM extensions - Introduced a new `ExtensionJobRegistry` to manage custom job types for extensions at runtime. - Added `host_register_job` function to facilitate job registration from WASM extensions. - Updated `PluginEnv` and `PluginManager` to include job registry functionality. - Enhanced the `job` macro to support automatic job registration during plugin initialization. - Updated documentation and tests to reflect the new job registration capabilities. --- core/src/infra/extension/host_functions.rs | 63 ++++++++ core/src/infra/extension/job_registry.rs | 137 ++++++++++++++++++ core/src/infra/extension/manager.rs | 16 ++ core/src/infra/extension/mod.rs | 2 + core/src/infra/job/manager.rs | 49 ++++++- core/tests/wasm_job_execution_test.rs | 30 ++-- crates/sdk-macros/src/extension.rs | 122 ++++++++++++---- crates/sdk-macros/src/job.rs | 42 +++++- crates/sdk/src/ffi.rs | 28 ++++ extensions/test-extension/src/lib.rs | 13 +- extensions/test-extension/test_extension.wasm | Bin 82388 -> 82836 bytes 11 files changed, 443 insertions(+), 59 deletions(-) create mode 100644 core/src/infra/extension/job_registry.rs diff --git a/core/src/infra/extension/host_functions.rs b/core/src/infra/extension/host_functions.rs index 7613cd678..8d48c46ec 100644 --- a/core/src/infra/extension/host_functions.rs +++ b/core/src/infra/extension/host_functions.rs @@ -21,6 +21,7 @@ pub struct PluginEnv { pub api_dispatcher: Arc, // For creating sessions pub permissions: ExtensionPermissions, pub memory: Memory, + pub job_registry: Arc, } /// THE MAIN HOST FUNCTION - Generic Wire RPC @@ -399,3 +400,65 @@ pub fn host_job_increment_items( tracing::debug!(extension = %plugin_env.extension_id, "Processed {} items", count); // TODO: Update metrics } + +// === Extension Registration Functions === + +/// Register a job type for an extension +/// +/// Called from plugin_init() to register custom job types +/// +/// # Arguments +/// - `job_name_ptr`, `job_name_len`: Job name (e.g., "email_scan") +/// - `export_fn_ptr`, `export_fn_len`: WASM export function (e.g., "execute_email_scan") +/// - `resumable`: Whether the job supports resumption (1 = yes, 0 = no) +/// +/// # Returns +/// 0 on success, 1 on error +pub fn host_register_job( + mut env: FunctionEnvMut, + job_name_ptr: WasmPtr, + job_name_len: u32, + export_fn_ptr: WasmPtr, + export_fn_len: u32, + resumable: u32, +) -> i32 { + let (plugin_env, mut store) = env.data_and_store_mut(); + let memory = &plugin_env.memory; + let memory_view = memory.view(&store); + + // Read job name + let job_name = match read_string_from_wasm(&memory_view, job_name_ptr, job_name_len) { + Ok(name) => name, + Err(e) => { + tracing::error!("Failed to read job name: {}", e); + return 1; // Error + } + }; + + // Read export function name + let export_fn = match read_string_from_wasm(&memory_view, export_fn_ptr, export_fn_len) { + Ok(name) => name, + Err(e) => { + tracing::error!("Failed to read export function name: {}", e); + return 1; // Error + } + }; + + let is_resumable = resumable != 0; + + // Register the job synchronously (no async needed) + let result = plugin_env.job_registry.register( + plugin_env.extension_id.clone(), + job_name, + export_fn, + is_resumable, + ); + + match result { + Ok(()) => 0, // Success + Err(e) => { + tracing::error!("Failed to register job: {}", e); + 1 // Error + } + } +} diff --git a/core/src/infra/extension/job_registry.rs b/core/src/infra/extension/job_registry.rs new file mode 100644 index 000000000..132341f44 --- /dev/null +++ b/core/src/infra/extension/job_registry.rs @@ -0,0 +1,137 @@ +//! Runtime job registry for WASM extensions +//! +//! Allows extensions to register custom job types at runtime that integrate +//! with the core job system. + +use crate::infra::extension::WasmJob; +use std::collections::HashMap; +use std::sync::RwLock; + +/// Metadata for a registered extension job +#[derive(Debug, Clone)] +pub struct ExtensionJobRegistration { + /// Extension ID (e.g., "finance") + pub extension_id: String, + /// Job name (e.g., "email_scan") + pub job_name: String, + /// Full qualified name (e.g., "finance:email_scan") + pub full_name: String, + /// WASM export function name (e.g., "execute_email_scan") + pub export_fn: String, + /// Whether this job supports resumption + pub resumable: bool, +} + +/// Runtime registry for extension-defined jobs +pub struct ExtensionJobRegistry { + /// Map from full job name (e.g., "finance:email_scan") to registration + jobs: RwLock>, +} + +impl ExtensionJobRegistry { + /// Create a new empty registry + pub fn new() -> Self { + Self { + jobs: RwLock::new(HashMap::new()), + } + } + + /// Register a new extension job + pub fn register( + &self, + extension_id: String, + job_name: String, + export_fn: String, + resumable: bool, + ) -> Result<(), String> { + let full_name = format!("{}:{}", extension_id, job_name); + + let registration = ExtensionJobRegistration { + extension_id: extension_id.clone(), + job_name: job_name.clone(), + full_name: full_name.clone(), + export_fn: export_fn.clone(), + resumable, + }; + + let mut jobs = self.jobs.write().unwrap(); + + // Check for duplicates + if jobs.contains_key(&full_name) { + return Err(format!( + "Job '{}' is already registered by extension '{}'", + job_name, extension_id + )); + } + + tracing::info!("Registered extension job: {} -> {}", full_name, export_fn); + + jobs.insert(full_name, registration); + Ok(()) + } + + /// Check if a job name is registered + pub fn has_job(&self, full_name: &str) -> bool { + self.jobs.read().unwrap().contains_key(full_name) + } + + /// Get registration info for a job + pub fn get_job(&self, full_name: &str) -> Option { + self.jobs.read().unwrap().get(full_name).cloned() + } + + /// Create a WasmJob instance from a registered job name + pub fn create_wasm_job(&self, full_name: &str, state_json: String) -> Result { + let registration = self + .get_job(full_name) + .ok_or_else(|| format!("Extension job '{}' not found", full_name))?; + + Ok(WasmJob { + extension_id: registration.extension_id, + export_fn: registration.export_fn, + state_json, + is_resuming: false, + }) + } + + /// List all registered jobs for an extension + pub fn list_jobs_for_extension(&self, extension_id: &str) -> Vec { + self.jobs + .read() + .unwrap() + .values() + .filter(|reg| reg.extension_id == extension_id) + .cloned() + .collect() + } + + /// List all registered extension jobs + pub fn list_all_jobs(&self) -> Vec { + self.jobs.read().unwrap().values().cloned().collect() + } + + /// Unregister all jobs for an extension (called on unload) + pub fn unregister_extension_jobs(&self, extension_id: &str) -> usize { + let mut jobs = self.jobs.write().unwrap(); + let before_count = jobs.len(); + + jobs.retain(|_, reg| reg.extension_id != extension_id); + + let removed = before_count - jobs.len(); + if removed > 0 { + tracing::info!( + "Unregistered {} job(s) for extension '{}'", + removed, + extension_id + ); + } + + removed + } +} + +impl Default for ExtensionJobRegistry { + fn default() -> Self { + Self::new() + } +} diff --git a/core/src/infra/extension/manager.rs b/core/src/infra/extension/manager.rs index 7e5be5f42..2a3d8c757 100644 --- a/core/src/infra/extension/manager.rs +++ b/core/src/infra/extension/manager.rs @@ -14,6 +14,7 @@ use wasmer::{imports, Function, FunctionEnv, Instance, Memory, Module, Store}; use crate::{context::CoreContext, infra::api::ApiDispatcher}; use super::host_functions::{self, host_spacedrive_call, host_spacedrive_log, PluginEnv}; +use super::job_registry::ExtensionJobRegistry; use super::permissions::ExtensionPermissions; use super::types::{ExtensionManifest, LoadedPlugin}; @@ -45,6 +46,7 @@ pub struct PluginManager { plugin_dir: PathBuf, core_context: Arc, api_dispatcher: Arc, + job_registry: Arc, } impl PluginManager { @@ -62,9 +64,15 @@ impl PluginManager { plugin_dir, core_context, api_dispatcher, + job_registry: Arc::new(ExtensionJobRegistry::new()), } } + /// Get the job registry for extension jobs + pub fn job_registry(&self) -> Arc { + self.job_registry.clone() + } + /// Load a WASM plugin from directory /// /// Expected structure: @@ -128,6 +136,7 @@ impl PluginManager { api_dispatcher: self.api_dispatcher.clone(), permissions, memory: temp_memory, + job_registry: self.job_registry.clone(), }; let env = FunctionEnv::new(&mut self.store, plugin_env); @@ -178,6 +187,13 @@ impl PluginManager { &env, host_functions::host_job_increment_items ), + + // Extension registration functions + "register_job" => Function::new_typed_with_env( + &mut self.store, + &env, + host_functions::host_register_job + ), } }; diff --git a/core/src/infra/extension/mod.rs b/core/src/infra/extension/mod.rs index c1e81453b..7b0c058fc 100644 --- a/core/src/infra/extension/mod.rs +++ b/core/src/infra/extension/mod.rs @@ -18,11 +18,13 @@ //! - `types`: Shared types and manifest format mod host_functions; +mod job_registry; mod manager; mod permissions; mod types; mod wasm_job; +pub use job_registry::{ExtensionJobRegistration, ExtensionJobRegistry}; pub use manager::PluginManager; pub use permissions::{ExtensionPermissions, PermissionError}; pub use types::{ExtensionManifest, PluginManifest}; diff --git a/core/src/infra/job/manager.rs b/core/src/infra/job/manager.rs index e7b623322..0e42358c7 100644 --- a/core/src/infra/job/manager.rs +++ b/core/src/infra/job/manager.rs @@ -114,17 +114,50 @@ impl JobManager { params: serde_json::Value, priority: JobPriority, ) -> JobResult { - // Check if job type is registered - if !REGISTRY.has_job(job_name) { - return Err(JobError::NotFound(format!( - "Job type '{}' not registered", - job_name - ))); + // Try core job registry first + if REGISTRY.has_job(job_name) { + // Create job instance from core registry + let erased_job = REGISTRY.create_job(job_name, params)?; + return self.dispatch_erased_job(job_name, erased_job, priority, None).await; } - // Create job instance - let erased_job = REGISTRY.create_job(job_name, params)?; + // Check if it's an extension job (contains colon) + if job_name.contains(':') { + // Try extension job registry + if let Some(plugin_manager) = self.context.get_plugin_manager().await { + let job_registry = plugin_manager.read().await.job_registry(); + if job_registry.has_job(job_name) { + // Extract state JSON from params + let state_json = serde_json::to_string(¶ms) + .map_err(|e| JobError::serialization(format!("Failed to serialize params: {}", e)))?; + + // Create WasmJob from registry + let wasm_job = job_registry + .create_wasm_job(job_name, state_json) + .map_err(|e| JobError::NotFound(e))?; + + // Dispatch the WasmJob + return self.dispatch_with_priority(wasm_job, priority, None).await; + } + } + } + + // Job not found in either registry + Err(JobError::NotFound(format!( + "Job type '{}' not registered", + job_name + ))) + } + + /// Helper method to dispatch an erased job (extracted from dispatch_by_name) + async fn dispatch_erased_job( + &self, + job_name: &str, + erased_job: Box, + priority: JobPriority, + action_context: Option, + ) -> JobResult { let job_id = JobId::new(); info!("Dispatching job {} ({}): {}", job_id, job_name, job_name); diff --git a/core/tests/wasm_job_execution_test.rs b/core/tests/wasm_job_execution_test.rs index f73222013..f18106996 100644 --- a/core/tests/wasm_job_execution_test.rs +++ b/core/tests/wasm_job_execution_test.rs @@ -2,7 +2,7 @@ //! //! Tests that we can dispatch and execute WASM jobs -use sd_core::{infra::extension::WasmJob, Core}; +use sd_core::Core; use std::path::PathBuf; use tempfile::TempDir; @@ -65,27 +65,19 @@ async fn test_dispatch_wasm_job() { tracing::info!("✅ Extension loaded"); - // 4. Create WasmJob - let wasm_job = WasmJob { - extension_id: "test-extension".to_string(), - export_fn: "execute_test_counter".to_string(), - state_json: serde_json::json!({ - "current": 0, - "target": 10, - "processed": [] - }) - .to_string(), - is_resuming: false, - }; - - tracing::info!("Created WasmJob"); - - // 4. Dispatch job + // 4. Dispatch job by name (auto-registered as "test-extension:counter") let job_handle = library .jobs() - .dispatch(wasm_job) + .dispatch_by_name( + "test-extension:counter", + serde_json::json!({ + "current": 0, + "target": 10, + "processed": [] + }), + ) .await - .expect("Should dispatch WasmJob"); + .expect("Should dispatch extension job by name"); tracing::info!("✅ WASM job dispatched: {}", job_handle.id()); diff --git a/crates/sdk-macros/src/extension.rs b/crates/sdk-macros/src/extension.rs index 6caaefb12..4bde45c17 100644 --- a/crates/sdk-macros/src/extension.rs +++ b/crates/sdk-macros/src/extension.rs @@ -2,41 +2,118 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Expr, ItemStruct, Lit, Meta}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Expr, Ident, ItemStruct, LitStr, Result, Token, +}; + +struct ExtensionArgs { + id: String, + name: String, + version: String, + jobs: Vec, +} + +impl Parse for ExtensionArgs { + fn parse(input: ParseStream) -> Result { + let mut id = None; + let mut name = None; + let mut version = None; + let mut jobs = Vec::new(); + + while !input.is_empty() { + let ident: Ident = input.parse()?; + input.parse::()?; + + match ident.to_string().as_str() { + "id" => { + let lit: LitStr = input.parse()?; + id = Some(lit.value()); + } + "name" => { + let lit: LitStr = input.parse()?; + name = Some(lit.value()); + } + "version" => { + let lit: LitStr = input.parse()?; + version = Some(lit.value()); + } + "jobs" => { + let content; + syn::bracketed!(content in input); + while !content.is_empty() { + jobs.push(content.parse()?); + if content.peek(Token![,]) { + content.parse::()?; + } + } + } + _ => return Err(input.error("unknown parameter")), + } + + if input.peek(Token![,]) { + input.parse::()?; + } + } + + Ok(ExtensionArgs { + id: id.ok_or_else(|| input.error("missing id parameter"))?, + name: name.ok_or_else(|| input.error("missing name parameter"))?, + version: version.ok_or_else(|| input.error("missing version parameter"))?, + jobs, + }) + } +} pub fn extension_impl(args: TokenStream, input: TokenStream) -> TokenStream { let input_struct = parse_macro_input!(input as ItemStruct); + let args = parse_macro_input!(args as ExtensionArgs); - // Parse attributes manually for syn 2.0 - let parser = syn::meta::parser(|meta| { - // We'll extract what we need here - Ok(()) + let ext_id = &args.id; + let ext_name = &args.name; + let ext_version = &args.version; + + // Generate job registration code + let job_registrations = args.jobs.iter().map(|job_fn| { + let register_fn = quote::format_ident!("__register_{}", job_fn); + quote! { + { + let (name, export_fn, resumable) = #register_fn(); + ::spacedrive_sdk::ffi::log_info(&format!("Registering job: {}", name)); + + if let Err(_) = ::spacedrive_sdk::ffi::register_job_with_host( + name, + export_fn, + resumable + ) { + ::spacedrive_sdk::ffi::log_error(&format!("Failed to register job: {}", name)); + return 1; + } + } + } }); - let _ = syn::parse::Parser::parse(parser, args); - - // For now, use default values - // TODO: Properly parse attributes with syn 2.0 API - let ext_id = "test-beautiful"; - let ext_name = "Test Extension (Beautiful API)"; - let ext_version = "0.1.0"; - - let struct_name = &input_struct.ident; - let expanded = quote! { #input_struct - // Generate plugin_init + // Generate plugin_init with auto-registration #[no_mangle] pub extern "C" fn plugin_init() -> i32 { + ::spacedrive_sdk::ffi::log_info(&format!( + "{} v{} initializing...", + #ext_name, + #ext_version + )); + + // Register all jobs + #(#job_registrations)* + ::spacedrive_sdk::ffi::log_info(&format!( "✓ {} v{} initialized!", #ext_name, #ext_version )); - // TODO: Auto-register jobs/queries/actions here - 0 // Success } @@ -49,16 +126,7 @@ pub fn extension_impl(args: TokenStream, input: TokenStream) -> TokenStream { )); 0 // Success } - - // Extension metadata (for manifest generation) - #[cfg(feature = "manifest")] - pub const EXTENSION_METADATA: ExtensionMetadata = ExtensionMetadata { - id: #ext_id, - name: #ext_name, - version: #ext_version, - }; }; TokenStream::from(expanded) } - diff --git a/crates/sdk-macros/src/job.rs b/crates/sdk-macros/src/job.rs index c55a15196..93b49dd41 100644 --- a/crates/sdk-macros/src/job.rs +++ b/crates/sdk-macros/src/job.rs @@ -2,11 +2,29 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, FnArg, ItemFn, Type}; +use syn::{parse_macro_input, FnArg, ItemFn, LitStr, Type}; -pub fn job_impl(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn job_impl(args: TokenStream, input: TokenStream) -> TokenStream { let input_fn = parse_macro_input!(input as ItemFn); + // Parse job name from args if provided + let job_name: Option = if !args.is_empty() { + // Try to parse as name = "value" + let args_str = args.to_string(); + if let Some(name_value) = args_str.strip_prefix("name = \"") { + if let Some(name) = name_value.strip_suffix("\"") { + Some(name.to_string()) + } else { + None + } + } else { + // Try direct string literal + syn::parse::(args).ok().map(|lit| lit.value()) + } + } else { + None + }; + // Extract function info let fn_name = &input_fn.sig.ident; let fn_attrs = &input_fn.attrs; @@ -18,11 +36,31 @@ pub fn job_impl(_args: TokenStream, input: TokenStream) -> TokenStream { // Expected signature: async fn name(ctx: &JobContext, state: &mut State) -> Result<()> let state_type = extract_state_type(&input_fn); + // Generate registration function if job name is provided + let registration_fn = if let Some(ref name) = job_name { + let export_name_str = export_name.to_string(); + let register_fn_name = syn::Ident::new(&format!("__register_{}", fn_name), fn_name.span()); + + quote! { + // Generate a registration helper function + // This will be called by plugin_init + #[doc(hidden)] + pub fn #register_fn_name() -> (&'static str, &'static str, bool) { + (#name, #export_name_str, true) + } + } + } else { + quote! {} + }; + let expanded = quote! { // Keep original function for internal use #(#fn_attrs)* #input_fn + // Registration helper function + #registration_fn + // Generate FFI export #[no_mangle] pub extern "C" fn #export_name( diff --git a/crates/sdk/src/ffi.rs b/crates/sdk/src/ffi.rs index 569c5e98e..9043124d2 100644 --- a/crates/sdk/src/ffi.rs +++ b/crates/sdk/src/ffi.rs @@ -6,6 +6,13 @@ #[link(wasm_import_module = "spacedrive")] extern "C" { fn spacedrive_log(level: u32, msg_ptr: *const u8, msg_len: usize); + fn register_job( + job_name_ptr: *const u8, + job_name_len: u32, + export_fn_ptr: *const u8, + export_fn_len: u32, + resumable: u32, + ) -> i32; } /// Log a message (info level) @@ -52,3 +59,24 @@ pub extern "C" fn wasm_free(ptr: *mut u8, size: i32) { unsafe { std::alloc::dealloc(ptr, layout) }; } } + +/// Register a job with the extension system +/// +/// Called automatically by #[extension] macro during plugin_init() +pub fn register_job_with_host(job_name: &str, export_fn: &str, resumable: bool) -> Result<(), ()> { + let result = unsafe { + register_job( + job_name.as_ptr(), + job_name.len() as u32, + export_fn.as_ptr(), + export_fn.len() as u32, + if resumable { 1 } else { 0 }, + ) + }; + + if result == 0 { + Ok(()) + } else { + Err(()) + } +} diff --git a/extensions/test-extension/src/lib.rs b/extensions/test-extension/src/lib.rs index 9706d55f5..260d78b38 100644 --- a/extensions/test-extension/src/lib.rs +++ b/extensions/test-extension/src/lib.rs @@ -7,9 +7,15 @@ use spacedrive_sdk::prelude::*; use spacedrive_sdk::{extension, job}; // Extension Definition -// The #[extension] macro generates plugin initialization and cleanup functions. +// The #[extension] macro generates plugin_init() and plugin_cleanup(). +// List jobs in the jobs parameter for automatic registration. -#[extension(id = "test-extension", name = "Test Extension", version = "0.1.0")] +#[extension( + id = "test-extension", + name = "Test Extension", + version = "0.1.0", + jobs = [test_counter], +)] struct TestExtension; // Job State Definition @@ -24,8 +30,9 @@ pub struct CounterState { // Job Implementation // The #[job] macro handles FFI bindings, serialization, and error handling. +// The name parameter enables automatic registration (extension-id:name format). -#[job] +#[job(name = "counter")] fn test_counter(ctx: &JobContext, state: &mut CounterState) -> Result<()> { ctx.log(&format!( "Starting counter (current: {}, target: {})", diff --git a/extensions/test-extension/test_extension.wasm b/extensions/test-extension/test_extension.wasm index ecde0ad6331cc4bac826d0c403519fadbe5e505f..389ee4a38d1fa8be3587c97e4e30d6a8b8badaca 100755 GIT binary patch delta 14954 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