Skip to main content

quickjs_oxide/engine/api/core/
convert.rs

1//! Conversions between JavaScript values and Rust types, and host functions
2//! over converted arguments.
3
4use std::any::Any;
5use std::rc::Rc;
6
7use super::RuntimeError;
8use super::function::{HostFunction, Scope};
9use super::ops::{Js, JsResult};
10use super::property::PropertyKey;
11use super::value::{CallableRef, ObjectRef, Value};
12use crate::engine::api::error::NativeErrorKind;
13use crate::engine::value::JsString;
14
15/// A Rust value made from a JavaScript value, as the specification's
16/// conversions make it (`ToNumber`, `ToString`, …). A conversion that may
17/// run JavaScript runs it through `js`.
18pub trait FromJs: Sized {
19    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self>;
20}
21
22/// A JavaScript value made from a Rust value.
23pub trait IntoJs {
24    fn into_js(self, js: &mut impl Js) -> JsResult<Value>;
25}
26
27fn type_error(js: &mut impl Js, message: &str) -> RuntimeError {
28    js.throw_error(NativeErrorKind::Type, message)
29}
30
31/// `ToInt32` of a number.
32fn int32(number: f64) -> i32 {
33    uint32(number) as i32
34}
35
36/// `ToUint32` of a number.
37fn uint32(number: f64) -> u32 {
38    if !number.is_finite() {
39        return 0;
40    }
41    number.trunc().rem_euclid(4_294_967_296.0) as u32
42}
43
44impl FromJs for Value {
45    fn from_js(_: &mut impl Js, value: Value) -> JsResult<Self> {
46        Ok(value)
47    }
48}
49
50/// `ToBoolean`.
51impl FromJs for bool {
52    fn from_js(_: &mut impl Js, value: Value) -> JsResult<Self> {
53        Ok(value.to_boolean())
54    }
55}
56
57/// `ToNumber`.
58impl FromJs for f64 {
59    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
60        match value {
61            Value::Int(value) => Ok(f64::from(value)),
62            Value::Float(value) => Ok(value),
63            other => js.to_number(&other),
64        }
65    }
66}
67
68/// `ToInt32`.
69impl FromJs for i32 {
70    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
71        match value {
72            Value::Int(value) => Ok(value),
73            other => f64::from_js(js, other).map(int32),
74        }
75    }
76}
77
78/// `ToUint32`.
79impl FromJs for u32 {
80    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
81        match value {
82            Value::Int(value) => Ok(value as u32),
83            other => f64::from_js(js, other).map(uint32),
84        }
85    }
86}
87
88/// `ToString`.
89impl FromJs for JsString {
90    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
91        match value {
92            Value::String(text) => Ok(text),
93            other => js.to_js_string(&other),
94        }
95    }
96}
97
98/// `ToString`, a lone surrogate replaced by U+FFFD.
99impl FromJs for String {
100    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
101        JsString::from_js(js, value).map(|text| text.to_utf8_lossy())
102    }
103}
104
105/// `undefined` and `null` are `None`.
106impl<T: FromJs> FromJs for Option<T> {
107    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
108        match value {
109            Value::Undefined | Value::Null => Ok(None),
110            other => T::from_js(js, other).map(Some),
111        }
112    }
113}
114
115/// An array-like: its `length` (`ToLength`), then each index; a non-object
116/// is a `TypeError`.
117impl<T: FromJs> FromJs for Vec<T> {
118    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
119        let Value::Object(object) = value else {
120            return Err(type_error(js, "not an object"));
121        };
122        let key = PropertyKey::from_js_string(JsString::from_static("length"));
123        let length = js.get_property(&object, &key)?;
124        let length = f64::from_js(js, length)?;
125        // `ToLength`.
126        let length = if length.is_nan() || length <= 0.0 {
127            0.0
128        } else {
129            length.trunc().min(9_007_199_254_740_991.0)
130        };
131        let mut items = Vec::new();
132        let mut index = 0.0;
133        while index < length {
134            let key = if index < f64::from(u32::MAX) {
135                PropertyKey::index(index as u32)
136            } else {
137                PropertyKey::from_js_string(JsString::try_from_utf8(&index.to_string())?)
138            };
139            let item = js.get_property(&object, &key)?;
140            items.push(T::from_js(js, item)?);
141            index += 1.0;
142        }
143        Ok(items)
144    }
145}
146
147/// An object; anything else is a `TypeError`.
148impl FromJs for ObjectRef {
149    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
150        match value {
151            Value::Object(object) => Ok(object),
152            _ => Err(type_error(js, "not an object")),
153        }
154    }
155}
156
157/// A callable object; anything else is a `TypeError`.
158impl FromJs for CallableRef {
159    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
160        if let Value::Object(object) = &value
161            && let Some(callable) = js.as_callable(object)?
162        {
163            return Ok(callable);
164        }
165        Err(type_error(js, "not a function"))
166    }
167}
168
169/// The value a host object holds; another value is a `TypeError`.
170impl<T: Any> FromJs for Rc<T> {
171    fn from_js(js: &mut impl Js, value: Value) -> JsResult<Self> {
172        if let Value::Object(object) = &value
173            && let Some(data) = js.host_data::<T>(object)?
174        {
175            return Ok(data);
176        }
177        let name = std::any::type_name::<T>();
178        let name = name.rsplit("::").next().unwrap_or(name);
179        Err(type_error(js, &format!("{name} object expected")))
180    }
181}
182
183impl IntoJs for Value {
184    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
185        Ok(self)
186    }
187}
188
189/// `undefined`.
190impl IntoJs for () {
191    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
192        Ok(Value::Undefined)
193    }
194}
195
196impl IntoJs for bool {
197    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
198        Ok(Value::Bool(self))
199    }
200}
201
202impl IntoJs for i32 {
203    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
204        Ok(Value::Int(self))
205    }
206}
207
208impl IntoJs for u32 {
209    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
210        Ok(Value::number(f64::from(self)))
211    }
212}
213
214impl IntoJs for f64 {
215    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
216        Ok(Value::number(self))
217    }
218}
219
220impl IntoJs for JsString {
221    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
222        Ok(Value::String(self))
223    }
224}
225
226impl IntoJs for String {
227    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
228        Ok(Value::String(JsString::try_from_utf8(&self)?))
229    }
230}
231
232impl IntoJs for &str {
233    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
234        Ok(Value::String(JsString::try_from_utf8(self)?))
235    }
236}
237
238impl IntoJs for ObjectRef {
239    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
240        Ok(Value::Object(self))
241    }
242}
243
244impl IntoJs for CallableRef {
245    fn into_js(self, _: &mut impl Js) -> JsResult<Value> {
246        Ok(Value::from(self))
247    }
248}
249
250/// `None` is `undefined`.
251impl<T: IntoJs> IntoJs for Option<T> {
252    fn into_js(self, js: &mut impl Js) -> JsResult<Value> {
253        match self {
254            Some(value) => value.into_js(js),
255            None => Ok(Value::Undefined),
256        }
257    }
258}
259
260/// An array of the converted items.
261impl<T: IntoJs> IntoJs for Vec<T> {
262    fn into_js(self, js: &mut impl Js) -> JsResult<Value> {
263        let items = self
264            .into_iter()
265            .map(|item| item.into_js(js))
266            .collect::<JsResult<Vec<_>>>()?;
267        js.new_array_from_values(items).map(Value::Object)
268    }
269}
270
271/// The value, or the error to throw.
272impl<T: IntoJs> IntoJs for Result<T, RuntimeError> {
273    fn into_js(self, js: &mut impl Js) -> JsResult<Value> {
274        self.and_then(|value| value.into_js(js))
275    }
276}
277
278/// Host functions over converted arguments: a closure over the [`Scope`]
279/// and up to eight arguments, each converted by [`FromJs`] (a missing one
280/// from `undefined`, extra ones ignored), whose result converts by
281/// [`IntoJs`].
282macro_rules! typed_host_functions {
283    ($(($($arg:ident),+))*) => {$(
284        impl<F, R, $($arg),+> HostFunction<($($arg,)+)> for F
285        where
286            F: Fn(&mut Scope<'_>, $($arg),+) -> JsResult<R> + 'static,
287            $($arg: FromJs,)+
288            R: IntoJs,
289        {
290            #[allow(non_snake_case)]
291            fn call(&self, scope: &mut Scope<'_>) -> JsResult<Value> {
292                let mut index = 0;
293                $(
294                    let value = scope.arg(index);
295                    let $arg = $arg::from_js(scope, value)?;
296                    index += 1;
297                )+
298                let _ = index;
299                self(scope, $($arg),+)?.into_js(scope)
300            }
301        }
302    )*};
303}
304
305typed_host_functions! {
306    (A0)
307    (A0, A1)
308    (A0, A1, A2)
309    (A0, A1, A2, A3)
310    (A0, A1, A2, A3, A4)
311    (A0, A1, A2, A3, A4, A5)
312    (A0, A1, A2, A3, A4, A5, A6)
313    (A0, A1, A2, A3, A4, A5, A6, A7)
314}