Skip to main content

hm_plugin_sdk/
host.rs

1//! Safe wrappers around the host functions imported via Extism's
2//! `host_fn!` block. Plugin code calls these instead of touching
3//! `extism-pdk` directly.
4
5// The `extern "ExtismHost"` block below is FFI to host imports; calling
6// those externs requires `unsafe`. The safe wrappers in this module are
7// the whole point of the file.
8#![allow(unsafe_code)]
9// The `extism_pdk::*` wildcard pulls in `Json`, `host_fn`, and other items
10// the `host_fn!` macro expansion expects to find in scope; enumerating them
11// here would duplicate the PDK's internal contract.
12#![allow(clippy::wildcard_imports)]
13// Every wrapper below returns a value that the plugin obviously wants
14// (`#[must_use]` on every getter is noise — the call sites are short and
15// the patterns are immediately recognisable).
16#![allow(clippy::must_use_candidate)]
17// `should_cancel` deliberately maps over the Result to extract the cancel
18// flag and falls back to `false` on host-fn error; `is_ok_and` would lose
19// the intent ("treat host-fn failure as 'not cancelled'").
20#![allow(clippy::map_unwrap_or)]
21
22use extism_pdk::*;
23use hm_plugin_protocol::host_abi::*;
24use hm_plugin_protocol::{BuildEvent, StdStream};
25
26#[host_fn]
27extern "ExtismHost" {
28    fn hm_log(level: Json<Level>, msg: String);
29    fn hm_emit_step_log(stream: Json<StdStream>, bytes: Vec<u8>);
30    fn hm_emit_event(event: Json<BuildEvent>);
31
32    fn hm_kv_get(scope: Json<KvScope>, key: String) -> Json<Option<Vec<u8>>>;
33    fn hm_kv_set(scope: Json<KvScope>, key: String, val: Vec<u8>);
34
35    fn hm_archive_read(args: Json<ArchiveReadArgs>) -> Vec<u8>;
36    fn hm_archive_total_size(id: Json<ArchiveId>) -> u64;
37    fn hm_fs_read_config(rel_path: String) -> Json<Option<Vec<u8>>>;
38
39    fn hm_unix_socket_connect(path: String) -> Json<SocketHandle>;
40    fn hm_socket_write(args: Json<SocketWriteArgs>) -> u64;
41    fn hm_socket_read(args: Json<SocketReadArgs>) -> Vec<u8>;
42    fn hm_socket_close(h: Json<SocketHandle>);
43
44    fn hm_keyring_get(args: Json<KeyringArgs>) -> Json<Option<String>>;
45    fn hm_keyring_set(args: Json<KeyringSetArgs>);
46    fn hm_keyring_delete(args: Json<KeyringArgs>);
47
48    fn hm_tty_prompt(args: Json<TtyPromptArgs>) -> String;
49    fn hm_tty_confirm(args: Json<TtyConfirmArgs>) -> bool;
50    fn hm_browser_open(url: String) -> bool;
51    fn hm_spawn_loopback(port: Json<Option<u16>>) -> Json<LoopbackHandle>;
52    fn hm_loopback_recv(args: Json<LoopbackRecvArgs>) -> Json<Option<CallbackData>>;
53
54    fn hm_should_cancel() -> u32;
55
56    fn hm_write_stdout(bytes: Vec<u8>);
57    fn hm_write_stderr(bytes: Vec<u8>);
58}
59
60pub use hm_plugin_protocol::ArchiveId;
61
62// ─── Safe API used by plugin code ───────────────────────────────────────────
63
64/// Log a diagnostic line into the host's tracing subscriber.
65///
66/// # Panics
67/// Never panics — Extism propagates host-fn errors as `Err` values,
68/// which we trap and ignore (logs are best-effort).
69pub fn log(level: Level, msg: &str) {
70    let _ = unsafe { hm_log(Json(level), msg.to_string()) };
71}
72
73pub fn emit_step_log(stream: StdStream, bytes: &[u8]) {
74    let _ = unsafe { hm_emit_step_log(Json(stream), bytes.to_vec()) };
75}
76
77pub fn emit_event(event: BuildEvent) {
78    let _ = unsafe { hm_emit_event(Json(event)) };
79}
80
81pub fn kv_get(scope: KvScope, key: &str) -> Option<Vec<u8>> {
82    let Json(v) = unsafe { hm_kv_get(Json(scope), key.into()) }.unwrap_or(Json(None));
83    v
84}
85
86pub fn kv_set(scope: KvScope, key: &str, val: &[u8]) {
87    let _ = unsafe { hm_kv_set(Json(scope), key.into(), val.to_vec()) };
88}
89
90pub fn archive_total_size(id: ArchiveId) -> u64 {
91    unsafe { hm_archive_total_size(Json(id)) }.unwrap_or(0)
92}
93
94pub fn archive_read(id: ArchiveId, offset: u64, max: u64) -> Vec<u8> {
95    unsafe { hm_archive_read(Json(ArchiveReadArgs { id, offset, max })) }.unwrap_or_default()
96}
97
98pub fn fs_read_config(rel_path: &str) -> Option<Vec<u8>> {
99    let Json(v) = unsafe { hm_fs_read_config(rel_path.into()) }.unwrap_or(Json(None));
100    v
101}
102
103pub fn unix_socket_connect(path: &str) -> Option<SocketHandle> {
104    unsafe { hm_unix_socket_connect(path.into()) }
105        .ok()
106        .map(|Json(h)| h)
107}
108
109pub fn socket_write(h: SocketHandle, bytes: &[u8]) -> u64 {
110    unsafe {
111        hm_socket_write(Json(SocketWriteArgs {
112            h,
113            bytes: bytes.to_vec(),
114        }))
115    }
116    .unwrap_or(0)
117}
118
119pub fn socket_read(h: SocketHandle, max: u64) -> Vec<u8> {
120    unsafe { hm_socket_read(Json(SocketReadArgs { h, max })) }.unwrap_or_default()
121}
122
123pub fn socket_close(h: SocketHandle) {
124    let _ = unsafe { hm_socket_close(Json(h)) };
125}
126
127pub fn keyring_get(service: &str, account: &str) -> Option<String> {
128    let Json(v) = unsafe {
129        hm_keyring_get(Json(KeyringArgs {
130            service: service.into(),
131            account: account.into(),
132        }))
133    }
134    .unwrap_or(Json(None));
135    v
136}
137
138pub fn keyring_set(service: &str, account: &str, secret: &str) {
139    let _ = unsafe {
140        hm_keyring_set(Json(KeyringSetArgs {
141            service: service.into(),
142            account: account.into(),
143            secret: secret.into(),
144        }))
145    };
146}
147
148pub fn keyring_delete(service: &str, account: &str) {
149    let _ = unsafe {
150        hm_keyring_delete(Json(KeyringArgs {
151            service: service.into(),
152            account: account.into(),
153        }))
154    };
155}
156
157pub fn tty_prompt(msg: &str, mask: bool) -> String {
158    unsafe {
159        hm_tty_prompt(Json(TtyPromptArgs {
160            msg: msg.into(),
161            mask,
162        }))
163    }
164    .unwrap_or_default()
165}
166
167pub fn tty_confirm(msg: &str, default: bool) -> bool {
168    unsafe {
169        hm_tty_confirm(Json(TtyConfirmArgs {
170            msg: msg.into(),
171            default,
172        }))
173    }
174    .unwrap_or(default)
175}
176
177pub fn browser_open(url: &str) -> bool {
178    unsafe { hm_browser_open(url.into()) }.unwrap_or(false)
179}
180
181pub fn write_stdout(bytes: &[u8]) {
182    let _ = unsafe { hm_write_stdout(bytes.to_vec()) };
183}
184
185pub fn write_stderr(bytes: &[u8]) {
186    let _ = unsafe { hm_write_stderr(bytes.to_vec()) };
187}
188
189pub fn spawn_loopback(port: Option<u16>) -> Option<LoopbackHandle> {
190    unsafe { hm_spawn_loopback(Json(port)) }
191        .ok()
192        .map(|Json(h)| h)
193}
194
195pub fn loopback_recv(h: LoopbackHandle, timeout_ms: u32) -> Option<CallbackData> {
196    let Json(v) =
197        unsafe { hm_loopback_recv(Json(LoopbackRecvArgs { h, timeout_ms })) }.unwrap_or(Json(None));
198    v
199}
200
201pub fn should_cancel() -> bool {
202    unsafe { hm_should_cancel() }
203        .map(|n| n != 0)
204        .unwrap_or(false)
205}