Skip to main content

yog_api/
service.rs

1//! Services through the bus: a mod answers calls to an address
2//! ([`Registry::provide`]), and calls whoever answers one ([`call`]).
3
4use std::os::raw::c_void;
5use yog_sync::Arc;
6
7use yog_abi::{YogApi, YogBytes, YogReply, YogStatus, YogStr, status};
8use yog_data::Schema;
9use yog_event::{EventPhase, Message, Service, keyed};
10
11use crate::bus::{EventCx, Subscription};
12use crate::registry::{Registry, global_api, guard};
13
14/// The bus's handler for provider `ud`: the request read as `S`, the
15/// mod's answer written as the reply.
16unsafe extern "C" fn on_call<S: Service, F>(
17    ud: *mut c_void,
18    payload: YogBytes,
19    reply: *const YogReply,
20) -> YogStatus
21where
22    F: Fn(&S, &EventCx) -> S::Reply + Send + Sync,
23{
24    let s = unsafe { &*(ud as *const Subscription<S, F>) };
25    let Some(request) = S::from_bytes(unsafe { payload.as_slice() }) else {
26        return status::BAD_PAYLOAD;
27    };
28    let Some(answer) = guard(None, || Some((s.handler())(&request, &s.cx()))) else {
29        return status::FAILED;
30    };
31    unsafe { YogReply::send(reply, &answer.to_bytes()) };
32    status::OK
33}
34
35impl Registry {
36    /// Answers every call to `S` — one provider per address: `false` if
37    /// someone answers it already.
38    pub fn provide<S: Service + 'static>(
39        &mut self,
40        handler: impl Fn(&S, &EventCx) -> S::Reply + Send + Sync + 'static,
41    ) -> bool {
42        self.providing::<S, _>(S::ADDRESS, Arc::new(handler))
43    }
44
45    /// [`Registry::provide`] for a keyed service's `key` only (`S::KEY`):
46    /// the inserts into one block.
47    pub fn provide_key<S: Service + 'static>(
48        &mut self,
49        key: &str,
50        handler: impl Fn(&S, &EventCx) -> S::Reply + Send + Sync + 'static,
51    ) -> bool {
52        self.providing::<S, _>(&keyed(S::ADDRESS, key), Arc::new(handler))
53    }
54
55    /// Provides `address` with a raw bus handler (a `#[yog_export]`'s).
56    pub(crate) fn provide_raw(&mut self, address: &str, handler: yog_abi::YogHandlerFn) -> bool {
57        let bus = unsafe { &*self.api().bus };
58        let st = unsafe {
59            let at = (bus.resolve)(bus.ctx, YogStr::from_str(address));
60            (bus.provide)(bus.ctx, at, std::ptr::null_mut(), handler)
61        };
62        if st != status::OK {
63            crate::error!("{address}: someone provides it already");
64        }
65        st == status::OK
66    }
67
68    /// Describes service `address` on the bus (`bus/declare`): what
69    /// `bus/describe` answers, what `bus/find` finds by `interfaces`.
70    pub(crate) fn declare(
71        &self,
72        address: &str,
73        doc: &str,
74        shape: &yog_data::Shape,
75        reply: &yog_data::Shape,
76        interfaces: Vec<String>,
77    ) {
78        use yog_event::bus::{AddressKind, Descriptor};
79        let d = Descriptor {
80            address: address.to_owned(),
81            kind: AddressKind::Service,
82            doc: doc.to_owned(),
83            shape: shape.describe(),
84            reply: Some(reply.describe()),
85            interfaces,
86        };
87        self.call(&d);
88    }
89
90    fn providing<S: Service + 'static, F>(&mut self, address: &str, handler: Arc<F>) -> bool
91    where
92        F: Fn(&S, &EventCx) -> S::Reply + Send + Sync + 'static,
93    {
94        let api = self.api();
95        let bus = unsafe { &*api.bus };
96        let sub = Box::into_raw(Box::new(Subscription::<S, F>::new(
97            handler,
98            EventPhase::Post,
99        )));
100        let st = unsafe {
101            let at = (bus.resolve)(bus.ctx, YogStr::from_str(address));
102            (bus.provide)(bus.ctx, at, sub.cast::<c_void>(), on_call::<S, F>)
103        };
104        if st != status::OK {
105            crate::error!("{address}: someone provides it already");
106            return false;
107        }
108        let doc = match S::shape() {
109            yog_data::Shape::Struct(s) => s.doc,
110            _ => "",
111        };
112        self.declare(S::ADDRESS, doc, &S::shape(), &S::Reply::shape(), Vec::new());
113        true
114    }
115}
116
117/// Calls `request`'s provider: its answer, `None` when no one answers (a
118/// service not on this host, or this mod not registered yet).
119pub fn call<S: Service>(request: &S) -> Option<S::Reply> {
120    let api = global_api();
121    if api.is_null() {
122        return None;
123    }
124    call_on(unsafe { &*api }, request)
125}
126
127impl Registry {
128    /// [`call`] while registering.
129    pub fn call<S: Service>(&self, request: &S) -> Option<S::Reply> {
130        call_on(self.api(), request)
131    }
132}
133
134fn call_on<S: Service>(api: &YogApi, request: &S) -> Option<S::Reply> {
135    let reply = request.with_bytes(|payload| call_bytes(api, request.address(), payload))?;
136    S::Reply::from_bytes(&reply)
137}
138
139/// Calls `address`'s provider with `payload`: its reply's bytes, `None`
140/// when no one answers (or this mod isn't registered yet).
141pub(crate) fn call_at(address: yog_event::Address, payload: &[u8]) -> Option<Vec<u8>> {
142    let api = global_api();
143    if api.is_null() {
144        return None;
145    }
146    call_bytes(unsafe { &*api }, address, payload)
147}
148
149/// The runtime's atom table (`yog_data::atom`), for this mod's copy of
150/// `yog-data`: the same numbers as every mod's and the host's.
151pub(crate) const ATOMS: yog_data::atom::Table = yog_data::atom::Table {
152    intern: atom,
153    name: atom_name,
154};
155
156fn atom(name: &str) -> u32 {
157    let bus = unsafe { &*(*global_api()).bus };
158    unsafe { (bus.atom)(bus.ctx, YogStr::from_str(name)) }
159}
160
161fn atom_name(n: u32) -> Option<&'static str> {
162    let bus = unsafe { &*(*global_api()).bus };
163    let s = unsafe { (bus.atom_name)(bus.ctx, n) };
164    // The runtime keeps its atoms' names for good.
165    (!s.ptr.is_null()).then(|| unsafe {
166        std::str::from_utf8_unchecked(std::slice::from_raw_parts(s.ptr, s.len as usize))
167    })
168}
169
170fn call_bytes(api: &YogApi, address: yog_event::Address, payload: &[u8]) -> Option<Vec<u8>> {
171    let bus = unsafe { &*api.bus };
172    let mut out = Vec::new();
173    let st = unsafe {
174        (bus.call)(
175            bus.ctx,
176            address,
177            YogBytes::new(payload),
178            &YogReply::to_vec(&mut out),
179        )
180    };
181    (st == status::OK).then_some(out)
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use yog_event::block::Insert;
188
189    /// Runs `on_call` for `handler` on `request`: its status and reply.
190    fn answer<F: Fn(&Insert, &EventCx) -> bool + Send + Sync>(
191        handler: F,
192        request: &[u8],
193    ) -> (YogStatus, Vec<u8>) {
194        let sub = Subscription::<Insert, F>::new(Arc::new(handler), EventPhase::Post);
195        let mut out = Vec::new();
196        let ud = (&sub as *const Subscription<Insert, F>)
197            .cast_mut()
198            .cast::<c_void>();
199        let st = unsafe {
200            on_call::<Insert, F>(ud, YogBytes::new(request), &YogReply::to_vec(&mut out))
201        };
202        (st, out)
203    }
204
205    fn insert(item: &str) -> Insert {
206        Insert {
207            block_id: "mymod:crate".into(),
208            pos: yog_core::BlockPos { x: 0, y: 64, z: 0 },
209            dimension: "minecraft:overworld".into(),
210            item_id: item.into(),
211            count: 1,
212            nbt: String::new(),
213            simulate: true,
214        }
215    }
216
217    #[test]
218    fn a_provider_answers_with_its_reply() {
219        let takes_stone = |i: &Insert, _: &EventCx| i.item_id == "minecraft:stone";
220        let (st, out) = answer(takes_stone, &insert("minecraft:stone").to_bytes());
221        assert_eq!((st, bool::from_bytes(&out)), (status::OK, Some(true)));
222        let (_, out) = answer(takes_stone, &insert("minecraft:dirt").to_bytes());
223        assert_eq!(bool::from_bytes(&out), Some(false));
224    }
225
226    #[test]
227    fn a_bad_request_or_a_panic_isnt_an_answer() {
228        let ok = |_: &Insert, _: &EventCx| true;
229        assert_eq!(answer(ok, b"junk").0, status::BAD_PAYLOAD);
230        let panics = |_: &Insert, _: &EventCx| -> bool { panic!("mod bug") };
231        assert_eq!(
232            answer(panics, &insert("minecraft:stone").to_bytes()).0,
233            status::FAILED
234        );
235    }
236}