Skip to main content

rns_hooks/
builtin.rs

1use std::collections::HashMap;
2use std::sync::{Arc, OnceLock, RwLock};
3
4use crate::engine_access::EngineAccess;
5use crate::error::HookError;
6use crate::hooks::HookContext;
7use crate::result::{ExecuteResult, HookResult, Verdict};
8use crate::runtime::RawProviderEvent;
9use crate::wire::ActionWire;
10
11/// Invocation data passed to a built-in hook.
12///
13/// Built-in hooks are regular Rust code linked into the host process. They are
14/// intended for first-party hooks and appliance builds where the hook code is
15/// trusted and should not require a WASM runtime or a dynamic library file.
16pub struct BuiltinHookCall<'a> {
17    pub ctx: &'a HookContext<'a>,
18    pub data_override: Option<&'a [u8]>,
19    pub engine_access: &'a dyn EngineAccess,
20    pub now: f64,
21    pub provider_events_enabled: bool,
22}
23
24/// Host-side effects a built-in hook may request during one invocation.
25#[derive(Default)]
26pub struct BuiltinHookHost {
27    injected_actions: Vec<ActionWire>,
28    provider_events: Vec<RawProviderEvent>,
29    modified_data: Option<Vec<u8>>,
30}
31
32impl BuiltinHookHost {
33    pub fn inject_action(&mut self, action: ActionWire) {
34        self.injected_actions.push(action);
35    }
36
37    pub fn emit_event(
38        &mut self,
39        call: &BuiltinHookCall<'_>,
40        payload_type: impl Into<String>,
41        payload: impl Into<Vec<u8>>,
42    ) -> Result<(), HookError> {
43        if !call.provider_events_enabled {
44            return Err(HookError::Trap(
45                "provider events are not enabled for this invocation".into(),
46            ));
47        }
48        self.provider_events.push(RawProviderEvent {
49            payload_type: payload_type.into(),
50            payload: payload.into(),
51        });
52        Ok(())
53    }
54
55    pub fn set_modified_data(&mut self, data: impl Into<Vec<u8>>) {
56        self.modified_data = Some(data.into());
57    }
58
59    fn finish(self, result: HookResult) -> ExecuteResult {
60        ExecuteResult {
61            hook_result: Some(result),
62            injected_actions: self.injected_actions,
63            provider_events: self
64                .provider_events
65                .into_iter()
66                .map(|event| crate::result::EmittedProviderEvent {
67                    hook_name: String::new(),
68                    payload_type: event.payload_type,
69                    payload: event.payload,
70                })
71                .collect(),
72            modified_data: self.modified_data,
73        }
74    }
75}
76
77pub trait BuiltinHook: Send + Sync + 'static {
78    fn call(
79        &self,
80        call: BuiltinHookCall<'_>,
81        host: &mut BuiltinHookHost,
82    ) -> Result<HookResult, HookError>;
83}
84
85impl<F> BuiltinHook for F
86where
87    F: for<'a, 'b> Fn(
88            BuiltinHookCall<'a>,
89            &'b mut BuiltinHookHost,
90        ) -> Result<HookResult, HookError>
91        + Send
92        + Sync
93        + 'static,
94{
95    fn call(
96        &self,
97        call: BuiltinHookCall<'_>,
98        host: &mut BuiltinHookHost,
99    ) -> Result<HookResult, HookError> {
100        self(call, host)
101    }
102}
103
104#[derive(Clone)]
105pub struct BuiltinProgram {
106    id: String,
107    hook: Arc<dyn BuiltinHook>,
108}
109
110impl BuiltinProgram {
111    pub fn load(id: impl Into<String>) -> Result<Self, HookError> {
112        let id = id.into();
113        let hook = resolve_builtin_hook(&id).ok_or_else(|| {
114            HookError::CompileError(format!("built-in hook '{}' is not registered", id))
115        })?;
116        Ok(Self { id, hook })
117    }
118
119    pub fn id(&self) -> &str {
120        &self.id
121    }
122
123    pub fn execute(
124        &self,
125        ctx: &HookContext<'_>,
126        data_override: Option<&[u8]>,
127        engine_access: &dyn EngineAccess,
128        now: f64,
129        provider_events_enabled: bool,
130    ) -> Result<ExecuteResult, HookError> {
131        let call = BuiltinHookCall {
132            ctx,
133            data_override,
134            engine_access,
135            now,
136            provider_events_enabled,
137        };
138        let mut host = BuiltinHookHost::default();
139        let result = self.hook.call(call, &mut host)?;
140        if Verdict::from_u32(result.verdict).is_none() {
141            return Err(HookError::InvalidResult(format!(
142                "invalid verdict value: {}",
143                result.verdict
144            )));
145        }
146        Ok(host.finish(result))
147    }
148}
149
150static BUILTIN_HOOKS: OnceLock<RwLock<HashMap<String, Arc<dyn BuiltinHook>>>> = OnceLock::new();
151
152fn registry() -> &'static RwLock<HashMap<String, Arc<dyn BuiltinHook>>> {
153    BUILTIN_HOOKS.get_or_init(|| RwLock::new(HashMap::new()))
154}
155
156/// Register a built-in hook implementation under a stable ID.
157///
158/// Third-party binaries can call this at startup after linking a plugin crate:
159///
160/// ```ignore
161/// rns_hooks::register_builtin_hook("vendor.my_hook", my_hook::hook)?;
162/// ```
163pub fn register_builtin_hook(
164    id: impl Into<String>,
165    hook: impl BuiltinHook,
166) -> Result<(), HookError> {
167    let id = id.into();
168    let mut hooks = registry()
169        .write()
170        .map_err(|_| HookError::CompileError("built-in hook registry poisoned".into()))?;
171    if hooks.contains_key(&id) {
172        return Err(HookError::CompileError(format!(
173            "built-in hook '{}' is already registered",
174            id
175        )));
176    }
177    hooks.insert(id, Arc::new(hook));
178    Ok(())
179}
180
181pub fn resolve_builtin_hook(id: &str) -> Option<Arc<dyn BuiltinHook>> {
182    registry().read().ok()?.get(id).cloned()
183}
184
185#[cfg(test)]
186pub fn clear_builtin_hooks_for_tests() {
187    if let Ok(mut hooks) = registry().write() {
188        hooks.clear();
189    }
190}