Skip to main content

intuicio_core/
host.rs

1//! The native side of a scripting solution.
2//!
3//! See [`Host`].
4use crate::{
5    Filter,
6    context::Context,
7    function::{FunctionHandle, FunctionQuery, FunctionQueryParameter, Parameters},
8    registry::{Registry, RegistryHandle},
9    types::TypeQuery,
10};
11use intuicio_data::data_stack::DataStackPack;
12use std::{cell::RefCell, marker::PhantomData, sync::Arc};
13use typid::ID;
14
15thread_local! {
16    /// Per-thread stack of hosts installed with [`Host::push_global`].
17    static GLOBAL_HOST_STACK: RefCell<Vec<(HostId, Host)>> = const{ RefCell::new(vec![]) };
18}
19
20/// Identifies a host pushed onto the global stack.
21pub type HostId = ID<Host>;
22
23/// A cloneable factory for hosts.
24///
25/// A [`Host`] cannot cross threads, but a producer can. Store one in the
26/// context's custom data and every worker thread can build a host of its own
27/// with the same registry.
28#[derive(Clone)]
29pub struct HostProducer {
30    producer: Arc<Box<dyn Fn() -> Host + Send + Sync>>,
31}
32
33impl HostProducer {
34    /// Wraps a closure that builds a host.
35    pub fn new(f: impl Fn() -> Host + Send + Sync + 'static) -> Self {
36        Self {
37            producer: Arc::new(Box::new(f)),
38        }
39    }
40
41    /// Builds a new host.
42    pub fn produce(&self) -> Host {
43        (self.producer)()
44    }
45}
46
47/// A [`Context`] and a [`Registry`] paired up, ready to call functions.
48///
49/// This is the application side of a scripting solution: register your native
50/// types and functions, install your scripts, then call in.
51///
52/// The registry is shared through an [`Arc`], so forks and worker threads all
53/// see the same definitions. It must be complete before the first call, since
54/// a registry in use can no longer be modified.
55pub struct Host {
56    context: Context,
57    registry: RegistryHandle,
58}
59
60impl Host {
61    /// Pairs a context with a registry.
62    pub fn new(context: Context, registry: RegistryHandle) -> Self {
63        Self { context, registry }
64    }
65
66    /// Builds a host with a fresh context of the same capacities, sharing this
67    /// registry.
68    pub fn fork(&self) -> Self {
69        Self {
70            context: self.context.fork(),
71            registry: self.registry.clone(),
72        }
73    }
74
75    /// Pushes this host onto the current thread's global stack and returns its
76    /// id.
77    ///
78    /// Gives the host back when the stack is already borrowed. Useful for code
79    /// that cannot pass a host down by argument, such as a native callback.
80    #[allow(clippy::result_large_err)]
81    pub fn push_global(self) -> Result<HostId, Self> {
82        GLOBAL_HOST_STACK.with(|host| match host.try_borrow_mut() {
83            Ok(mut stack) => {
84                let id = HostId::new();
85                stack.push((id, self));
86                Ok(id)
87            }
88            Err(_) => Err(self),
89        })
90    }
91
92    /// Takes the topmost host off the current thread's global stack.
93    pub fn pop_global() -> Option<Self> {
94        GLOBAL_HOST_STACK.with(move |stack| Some(stack.try_borrow_mut().ok()?.pop()?.1))
95    }
96
97    /// Takes a specific host off the current thread's global stack.
98    pub fn remove_global(id: HostId) -> Option<Self> {
99        GLOBAL_HOST_STACK.with(move |stack| {
100            let mut stack = stack.try_borrow_mut().ok()?;
101            let index = stack.iter().position(|(host_id, _)| host_id == &id)?;
102            Some(stack.remove(index).1)
103        })
104    }
105
106    /// Runs `f` with the topmost host of the current thread.
107    ///
108    /// Returns [`None`] when the stack is empty or already borrowed.
109    pub fn with_global<T>(f: impl FnOnce(&mut Self) -> T) -> Option<T> {
110        GLOBAL_HOST_STACK.with(move |stack| {
111            let mut stack = stack.try_borrow_mut().ok()?;
112            let host = &mut stack.last_mut()?.1;
113            Some(f(host))
114        })
115    }
116
117    /// Returns the context.
118    pub fn context(&mut self) -> &mut Context {
119        &mut self.context
120    }
121
122    /// Returns the registry.
123    pub fn registry(&self) -> &Registry {
124        &self.registry
125    }
126
127    /// Returns context and registry at once, as function bodies need both.
128    pub fn context_and_registry(&mut self) -> (&mut Context, &Registry) {
129        (&mut self.context, &self.registry)
130    }
131
132    /// Looks a function up by name, module and optionally owning type.
133    pub fn find_function(
134        &self,
135        name: &str,
136        module_name: &str,
137        type_name: Option<&str>,
138    ) -> Option<FunctionHandle> {
139        self.registry.find_function(FunctionQuery {
140            name: Some(name.into()),
141            module_name: Filter::Matching(module_name.into()),
142            type_query: type_name
143                .map(|type_name| TypeQuery {
144                    name: Some(type_name.into()),
145                    ..Default::default()
146                })
147                .into(),
148            ..Default::default()
149        })
150    }
151
152    /// Prepares a call to a function, matching it on argument and result types
153    /// as well as its name.
154    ///
155    /// Returns [`None`] when no function matches. Call
156    /// [`HostFunctionCall::run`] on the result with the arguments.
157    ///
158    /// ```no_run
159    /// # use intuicio_core::host::Host;
160    /// # fn example(host: &mut Host) {
161    /// let (result,) = host
162    ///     .call_function::<(i32,), _>("add", "lib", None)
163    ///     .unwrap()
164    ///     .run((40_i32, 2_i32));
165    /// # }
166    /// ```
167    pub fn call_function<O: DataStackPack, I: DataStackPack>(
168        &'_ mut self,
169        name: &str,
170        module_name: &str,
171        type_name: Option<&str>,
172    ) -> Option<HostFunctionCall<'_, I, O>> {
173        let inputs_query = I::pack_types()
174            .into_iter()
175            .map(|type_hash| FunctionQueryParameter {
176                type_query: Some(TypeQuery {
177                    type_hash: Some(type_hash),
178                    ..Default::default()
179                }),
180                ..Default::default()
181            })
182            .collect::<Vec<_>>();
183        let outputs_query = O::pack_types()
184            .into_iter()
185            .map(|type_hash| FunctionQueryParameter {
186                type_query: Some(TypeQuery {
187                    type_hash: Some(type_hash),
188                    ..Default::default()
189                }),
190                ..Default::default()
191            })
192            .collect::<Vec<_>>();
193        let handle = self.registry.find_function(FunctionQuery {
194            name: Some(name.into()),
195            module_name: Filter::Matching(module_name.into()),
196            type_query: type_name
197                .map(|type_name| TypeQuery {
198                    name: Some(type_name.into()),
199                    ..Default::default()
200                })
201                .into(),
202            inputs: Parameters::Exact(inputs_query.into()),
203            outputs: Parameters::Exact(outputs_query.into()),
204            ..Default::default()
205        })?;
206        Some(HostFunctionCall {
207            context: &mut self.context,
208            registry: &self.registry,
209            handle,
210            _phantom: Default::default(),
211        })
212    }
213}
214
215/// A function found by [`Host::call_function`], waiting for its arguments.
216pub struct HostFunctionCall<'a, I: DataStackPack, O: DataStackPack> {
217    context: &'a mut Context,
218    registry: &'a Registry,
219    handle: FunctionHandle,
220    _phantom: PhantomData<(I, O)>,
221}
222
223impl<I: DataStackPack, O: DataStackPack> HostFunctionCall<'_, I, O> {
224    /// Pushes the arguments, runs the function and pops the results.
225    pub fn run(self, inputs: I) -> O {
226        self.handle.call(self.context, self.registry, inputs, false)
227    }
228}