homecore_plugins/host_abi.rs
1//! Host ABI — the public on-the-wire memory format between the HOMECORE host
2//! and every WASM plugin.
3//!
4//! # Overview
5//!
6//! HOMECORE uses **JSON over UTF-8 linear memory** for all host↔guest data.
7//! This matches HA's JSON-everywhere convention and makes call payloads
8//! inspectable in debuggers without a schema file. Each `hc_*` host function
9//! and each guest export uses the same pointer + length convention:
10//!
11//! ```text
12//! host calls alloc(size) → ptr (exported by guest)
13//! host writes UTF-8 bytes into guest linear memory at [ptr, ptr+size)
14//! host calls the guest export with (ptr: i32, len: i32)
15//! guest reads and JSON-decodes the slice
16//! guest writes its reply via hc_state_set / hc_log / etc. (host imports)
17//! host calls dealloc(ptr, size) when finished (exported by guest)
18//! ```
19//!
20//! # Wire types
21//!
22//! | Call | Direction | JSON schema |
23//! |------|-----------|-------------|
24//! | `hc_state_get` reply | host → caller | `{"entity_id":"…","state":"…","attributes":{…}}` or null bytes (not found) |
25//! | `hc_state_set` args | guest → host | `(entity_id, state, attrs)` as 3 separate ptr/len pairs; each is a UTF-8 string or JSON object |
26//! | `hc_log` args | guest → host | `(level: i32, msg)` where level 0=debug 1=info 2=warn 3=error |
27//! | `hc_state_subscribe` | guest → host | entity_id UTF-8 string |
28//! | `setup_entry` | host → guest | `{"entry_id":"…","domain":"…","data":{}}` (ConfigEntry JSON) |
29//! | `receive_event` | host → guest | `{"event_type":"state_changed","entity_id":"…","new_state":"…"}` |
30//!
31//! # Memory layout guarantees
32//!
33//! - Buffers are **always** valid UTF-8 (JSON subset).
34//! - Maximum buffer size is **64 KiB** (65,536 bytes). Larger payloads must
35//! be split by the caller; the host rejects oversized writes with a WASM
36//! trap. This bound is enforced in [`write_guest_buf`].
37//! - The host **never** holds a guest memory pointer across a WASM call
38//! boundary. Pointers are only valid for the duration of a single call.
39//!
40//! # `hc_state_subscribe` semantics
41//!
42//! A plugin calls `hc_state_subscribe(eid_ptr, eid_len)` once per entity it
43//! wants to track. Subsequent state changes for that entity arrive via a
44//! `receive_event` call with event_type `"state_changed"`.
45//!
46//! Subscriptions are held for the lifetime of the plugin instance.
47
48/// Maximum number of bytes the host will write into a single guest buffer.
49/// Plugins may safely size their `alloc` buffers at this ceiling.
50pub const MAX_ABI_BUFFER_BYTES: usize = 65_536;
51
52/// JSON payload passed to `setup_entry` when a config entry is set up.
53///
54/// Serialises to HA-compat `ConfigEntry` JSON.
55#[derive(Debug, serde::Serialize, serde::Deserialize)]
56pub struct ConfigEntryJson {
57 pub entry_id: String,
58 pub domain: String,
59 pub title: String,
60 pub data: serde_json::Value,
61}
62
63impl ConfigEntryJson {
64 /// Construct a minimal config entry for test / bootstrap use.
65 pub fn bootstrap(domain: &str) -> Self {
66 Self {
67 entry_id: uuid::Uuid::new_v4().to_string(),
68 domain: domain.to_owned(),
69 title: domain.to_owned(),
70 data: serde_json::json!({}),
71 }
72 }
73}
74
75/// JSON payload for `receive_event` — `state_changed` variant.
76#[derive(Debug, serde::Serialize, serde::Deserialize)]
77pub struct StateChangedEventJson {
78 pub event_type: String,
79 pub entity_id: String,
80 pub new_state: Option<String>,
81 pub attributes: serde_json::Value,
82}
83
84impl StateChangedEventJson {
85 /// Construct a `state_changed` event payload.
86 pub fn state_changed(
87 entity_id: &str,
88 new_state: Option<&str>,
89 attributes: serde_json::Value,
90 ) -> Self {
91 Self {
92 event_type: "state_changed".to_owned(),
93 entity_id: entity_id.to_owned(),
94 new_state: new_state.map(str::to_owned),
95 attributes,
96 }
97 }
98}
99
100/// Log levels for `hc_log`.
101#[repr(i32)]
102pub enum LogLevel {
103 Debug = 0,
104 Info = 1,
105 Warn = 2,
106 Error = 3,
107}
108
109impl LogLevel {
110 /// Convert from the i32 wire value. Unknown values map to `Warn`.
111 pub fn from_i32(n: i32) -> Self {
112 match n {
113 0 => LogLevel::Debug,
114 1 => LogLevel::Info,
115 3 => LogLevel::Error,
116 _ => LogLevel::Warn,
117 }
118 }
119
120 pub fn as_str(&self) -> &'static str {
121 match self {
122 LogLevel::Debug => "DEBUG",
123 LogLevel::Info => "INFO",
124 LogLevel::Warn => "WARN",
125 LogLevel::Error => "ERROR",
126 }
127 }
128}