ort_web/lib.rs
1//! `ort-web` is an [`ort`] backend that enables the usage of ONNX Runtime in the web.
2//!
3//! # Usage
4//! ## CORS
5//! `ort-web` dynamically fetches the required scripts & WASM binary at runtime. By default, it will fetch the build
6//! from the `cdn.pyke.io` domain, so make sure it is accessible via CORS if you have that configured.
7//!
8//! You can also use a self-hosted build with [`Dist`]; see the [`api`](fn@api) function for an example. The scripts &
9//! binary can be acquired from the `dist` folder of the [`onnxruntime-web` npm package](https://npmjs.com/package/onnxruntime-web).
10//!
11//! ### Telemetry
12//! `ort-web` collects telemetry data by default and sends it to `signal.pyke.io`. This telemetry data helps us
13//! understand how `ort-web` is being used so we can improve it. Zero PII is collected; you can see what is sent in
14//! `_telemetry.js`. If you wish to contribute telemetry data, please allowlist `signal.pyke.io`; otherwise, it can be
15//! disabled via [`EnvironmentBuilder::with_telemetry`](ort::environment::EnvironmentBuilder::with_telemetry).
16//!
17//! ## Initialization
18//! `ort` must have the `alternative-backend` feature enabled, as this enables the usage of [`ort::set_api`].
19//!
20//! You can choose which build of ONNX Runtime to fetch by choosing any combination of these 3 feature flags:
21//! [`FEATURE_WEBGL`], [`FEATURE_WEBGPU`], [`FEATURE_WEBNN`]. These enable the usage of the [WebGL][ort::ep::WebGL],
22//! [WebGPU][ort::ep::WebGPU], and [WebNN][ort::ep::WebNN] EPs respectively. You can `|` features together to enable
23//! multiple at once:
24//!
25//! ```no_run
26//! use ort_web::{FEATURE_WEBGL, FEATURE_WEBGPU};
27//! ort::set_api(ort_web::api(FEATURE_WEBGL | FEATURE_WEBGPU).await?);
28//! ```
29//!
30//! You'll still need to configure the EPs on a per-session basis later like you would normally, but this allows you to
31//! e.g. only fetch the CPU build if the user doesn't have hardware acceleration.
32//!
33//! ## Session creation
34//! Sessions can only be created from a URL, or indirectly from memory - that means no
35//! `SessionBuilder::commit_from_memory_directly` for `.ort` format models, and no `SessionBuilder::commit_from_file`.
36//!
37//! The remaining commit functions - `SessionBuilder::commit_from_url` and `SessionBuilder::commit_from_memory` are
38//! marked `async` and need to be `await`ed. `commit_from_url` is always available when targeting WASM and does not
39//! require the `fetch-models` feature flag to be enabled for `ort`.
40//!
41//! ## Inference
42//! Only `Session::run_async` is supported; `Session::run` will always throw an error.
43//!
44//! Inference outputs are not synchronized by default (see the next section). If you need access to the data of all
45//! session outputs from Rust, the [`sync_outputs`] function can be used to sync them all at once.
46//!
47//! ## Synchronization
48//! ONNX Runtime is loaded as a separate WASM module, and `ort-web` acts as an intermediary between the two. There is no
49//! mechanism in WASM for two modules to share memory, so tensors often need to be 'synchronized' when one side needs to
50//! see data from the other.
51//!
52//! [`Tensor::new`](ort::value::Tensor::new) should never be used for creating inputs, as they start out allocated on
53//! the ONNX Runtime side, thus requiring a sync (of empty data) to Rust before it can be written to. Prefer instead
54//! [`Tensor::from_array`](ort::value::Tensor::from_array)/
55//! [`TensorRef::from_array_view`](ort::value::TensorRef::from_array_view), as tensors created this way never require
56//! synchronization.
57//!
58//! As previously stated, session outputs are **not** synchronized. If you wish to use their data in Rust, you must
59//! either sync all outputs at once with [`sync_outputs`], or sync each tensor at a time (if you only use a few
60//! outputs):
61//! ```ignore
62//! use ort_web::{TensorExt, SyncDirection};
63//!
64//! let mut outputs = session.run_async(ort::inputs![...]).await?;
65//!
66//! let mut bounding_boxes = outputs.remove("bounding_boxes").unwrap();
67//! bounding_boxes.sync(SyncDirection::Rust).await?;
68//!
69//! // now we can use the data
70//! let data = bounding_boxes.try_extract_tensor::<f32>()?;
71//! ```
72//!
73//! Once a session output is `sync`ed, that tensor becomes backed by a Rust buffer. Updates to the tensor's data from
74//! the Rust side will not reflect in ONNX Runtime until the tensor is `sync`ed with `SyncDirection::Runtime`. Likewise,
75//! updates to the tensor's data from ONNX Runtime won't reflect in Rust until Rust syncs that tensor with
76//! `SyncDirection::Rust`. You don't have to worry about this behavior if you only ever *read* from session outputs,
77//! though.
78//!
79//! ## Limitations
80//! - [`OutputSelector`](ort::session::run_options::OutputSelector) is not currently implemented.
81//! - [`IoBinding`](ort::io_binding) is not supported by ONNX Runtime on the web.
82
83#![deny(clippy::panic, clippy::panicking_unwrap)]
84#![warn(clippy::std_instead_of_alloc, clippy::std_instead_of_core)]
85
86extern crate alloc;
87extern crate core;
88
89use alloc::string::String;
90use core::fmt;
91
92use serde::Serialize;
93use wasm_bindgen::prelude::*;
94
95use crate::util::value_to_string;
96
97mod api;
98mod binding;
99mod env;
100mod memory;
101mod session;
102mod tensor;
103mod util;
104#[macro_use]
105pub(crate) mod private;
106
107pub use self::{
108 session::sync_outputs,
109 tensor::{SyncDirection, ValueExt}
110};
111
112pub type Result<T, E = Error> = core::result::Result<T, E>;
113
114#[derive(Debug, Clone)]
115pub struct Error {
116 msg: String
117}
118
119impl Error {
120 pub(crate) fn new(msg: impl Into<String>) -> Self {
121 Self { msg: msg.into() }
122 }
123}
124
125impl From<JsValue> for Error {
126 fn from(value: JsValue) -> Self {
127 Self::new(value_to_string(&value))
128 }
129}
130
131impl fmt::Display for Error {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 self.msg.fmt(f)
134 }
135}
136
137impl core::error::Error for Error {}
138
139/// Do not enable any execution provider features (CPU-only).
140pub const FEATURE_NONE: u8 = 0;
141/// Enable the WebGL execution provider for hardware acceleration.
142///
143/// See: <https://caniuse.com/webgl2>
144pub const FEATURE_WEBGL: u8 = 1 << 0;
145/// Enable the WebGPU execution provider for hardware acceleration.
146///
147/// See: <https://caniuse.com/webgpu>
148pub const FEATURE_WEBGPU: u8 = 1 << 1;
149/// Enable the WebNN execution provider for hardware acceleration.
150///
151/// See: <https://webmachinelearning.github.io/webnn-status/>
152pub const FEATURE_WEBNN: u8 = FEATURE_WEBGPU;
153
154/// Loads an `ort`-compatible ONNX Runtime API from `config`.
155///
156/// Returns an error if:
157/// - The requested feature set is not supported by `ort-web`.
158/// - The JavaScript/WASM modules fail to load.
159///
160/// `config` can be a feature set, in which case the default pyke-hosted builds will be used:
161/// ```no_run
162/// use ort::session::Session;
163/// use ort_web::{FEATURE_WEBGL, FEATURE_WEBGPU};
164///
165/// async fn init_model() -> anyhow::Result<Session> {
166/// // This must be called at least once before using any `ort` API.
167/// ort::set_api(ort_web::api(FEATURE_WEBGL | FEATURE_WEBGPU).await?);
168///
169/// let session = Session::builder()?.commit_from_url("https://...").await?;
170/// Ok(session)
171/// }
172/// ```
173///
174/// You can also use [`Dist`] to self-host the build:
175/// ```no_run
176/// use ort::session::Session;
177/// use ort_web::Dist;
178///
179/// async fn init_model() -> anyhow::Result<Session> {
180/// let dist = Dist::new("https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/")
181/// // load the WebGPU build
182/// .with_script_name("ort.webgpu.min.js");
183/// ort::set_api(ort_web::api(dist).await?);
184/// }
185/// ```
186pub async fn api<L: Loadable>(config: L) -> Result<ort_sys::OrtApi> {
187 let (features, dist) = config.into_features_and_dist()?;
188 binding::init_runtime(features, dist).await?;
189
190 Ok(self::api::api())
191}
192
193pub trait Loadable {
194 #[doc(hidden)]
195 fn into_features_and_dist(self) -> Result<(u8, JsValue)>;
196}
197
198impl Loadable for u8 {
199 fn into_features_and_dist(self) -> Result<(u8, JsValue)> {
200 Ok((self, JsValue::null()))
201 }
202}
203
204impl Loadable for Dist {
205 fn into_features_and_dist(self) -> Result<(u8, JsValue)> {
206 Ok((0, serde_wasm_bindgen::to_value(&self).map_err(|e| Error::new(e.to_string()))?))
207 }
208}
209
210#[derive(Default, Debug, Serialize, Clone)]
211#[serde(rename_all = "camelCase")]
212pub struct Integrities {
213 main: Option<String>,
214 wrapper: Option<String>,
215 binary: Option<String>
216}
217
218impl Integrities {
219 /// Set the SHA-384 SRI hash for the main (entrypoint) script.
220 pub fn set_main(&mut self, hash: impl Into<String>) {
221 self.main = Some(hash.into());
222 }
223
224 /// Set the SHA-384 SRI hash for the Emscripten wrapper script.
225 pub fn set_wrapper(&mut self, hash: impl Into<String>) {
226 self.wrapper = Some(hash.into());
227 }
228
229 /// Set the SHA-384 SRI hash for the WASM binary.
230 pub fn set_binary(&mut self, hash: impl Into<String>) {
231 self.binary = Some(hash.into());
232 }
233}
234
235#[derive(Debug, Serialize, Clone)]
236#[serde(rename_all = "camelCase")]
237pub struct Dist {
238 base_url: String,
239 script_name: String,
240 binary_name: Option<String>,
241 wrapper_name: Option<String>,
242 integrities: Integrities
243}
244
245impl Dist {
246 pub fn new(base_url: impl Into<String>) -> Self {
247 Self {
248 base_url: base_url.into(),
249 script_name: "ort.wasm.min.js".to_string(),
250 binary_name: None,
251 wrapper_name: None,
252 integrities: Integrities::default()
253 }
254 }
255
256 /// Configures the name of the entrypoint script file; defaults to `"ort.wasm.min.js"`.
257 pub fn with_script_name(mut self, name: impl Into<String>) -> Self {
258 self.script_name = name.into();
259 self
260 }
261
262 /// Enables preloading the WASM binary loaded by the entrypoint script.
263 pub fn with_binary_name(mut self, name: impl Into<String>) -> Self {
264 self.binary_name = Some(name.into());
265 self
266 }
267
268 /// Configures the name of the Emscripten wrapper script preloaded along with the WASM binary, if preloading is
269 /// enabled. Defaults to the binary name with the `.wasm` extension replaced with `.mjs`.
270 pub fn with_wrapper_name(mut self, name: impl Into<String>) -> Self {
271 self.wrapper_name = Some(name.into());
272 self
273 }
274
275 /// Modify Subresource Integrity (SRI) hashes.
276 pub fn integrities(&mut self) -> &mut Integrities {
277 &mut self.integrities
278 }
279}