@@ -0,0 +1,1293 @@
1+//! macOS JavaScriptCore implementation of the synchronous native host.
2+//!
3+//! This uses Apple's supported C API directly. Keeping the bridge in C values
4+//! avoids serializing drawing calls and lets path values stay in the JavaScript
5+//! array until Skia consumes each numeric element.
6+
7+use super::{
8+ absolute_path, failure, write_output, Config, Host, HostResult, Terminal, INTERRUPT, RUN_LOCK,
9+};
10+use std::cell::RefCell;
11+use std::collections::HashMap;
12+use std::error::Error;
13+use std::ffi::{c_char, c_void};
14+use std::io::{self, IsTerminal};
15+use std::panic::{catch_unwind, AssertUnwindSafe};
16+use std::ptr;
17+use std::rc::Rc;
18+use std::sync::atomic::{AtomicBool, Ordering};
19+use std::sync::Arc;
20+use std::time::Instant;
21+
22+use super::graphics;
23+
24+#[allow(non_camel_case_types)]
25+type JSContextRef = *const OpaqueJSContext;
26+#[allow(non_camel_case_types)]
27+type JSContextGroupRef = *const OpaqueJSContextGroup;
28+#[allow(non_camel_case_types)]
29+type JSGlobalContextRef = *mut OpaqueJSContext;
30+#[allow(non_camel_case_types)]
31+type JSStringRef = *mut OpaqueJSString;
32+#[allow(non_camel_case_types)]
33+type JSValueRef = *const OpaqueJSValue;
34+#[allow(non_camel_case_types)]
35+type JSObjectRef = *mut OpaqueJSValue;
36+
37+#[repr(C)]
38+struct OpaqueJSContext {
39+ _private: [u8; 0],
40+}
41+#[repr(C)]
42+struct OpaqueJSContextGroup {
43+ _private: [u8; 0],
44+}
45+#[repr(C)]
46+struct OpaqueJSString {
47+ _private: [u8; 0],
48+}
49+#[repr(C)]
50+struct OpaqueJSValue {
51+ _private: [u8; 0],
52+}
53+
54+type JSObjectCallAsFunctionCallback = unsafe extern "C" fn(
55+ JSContextRef,
56+ JSObjectRef,
57+ JSObjectRef,
58+ usize,
59+ *const JSValueRef,
60+ *mut JSValueRef,
61+) -> JSValueRef;
62+type JSShouldTerminateCallback = unsafe extern "C" fn(JSContextRef, *mut c_void) -> bool;
63+
64+#[link(name = "JavaScriptCore", kind = "framework")]
65+unsafe extern "C" {
66+ fn JSContextGroupCreate() -> JSContextGroupRef;
67+ fn JSContextGroupRelease(group: JSContextGroupRef);
68+ // Declared by JavaScriptCore's C API watchdog header on macOS. The SDK's
69+ // umbrella header omits it, but the framework exports it since macOS 10.6.
70+ fn JSContextGroupSetExecutionTimeLimit(
71+ group: JSContextGroupRef,
72+ limit: f64,
73+ callback: Option<JSShouldTerminateCallback>,
74+ context: *mut c_void,
75+ );
76+ fn JSContextGroupClearExecutionTimeLimit(group: JSContextGroupRef);
77+ fn JSGlobalContextCreateInGroup(
78+ group: JSContextGroupRef,
79+ class: *const c_void,
80+ ) -> JSGlobalContextRef;
81+ fn JSGlobalContextRelease(context: JSGlobalContextRef);
82+ fn JSContextGetGroup(context: JSContextRef) -> JSContextGroupRef;
83+ fn JSContextGetGlobalObject(context: JSContextRef) -> JSObjectRef;
84+ fn JSEvaluateScript(
85+ context: JSContextRef,
86+ script: JSStringRef,
87+ this_object: JSObjectRef,
88+ source_url: JSStringRef,
89+ starting_line: i32,
90+ exception: *mut JSValueRef,
91+ ) -> JSValueRef;
92+ fn JSObjectMake(context: JSContextRef, class: *const c_void, data: *mut c_void) -> JSObjectRef;
93+ fn JSObjectMakeArray(
94+ context: JSContextRef,
95+ count: usize,
96+ values: *const JSValueRef,
97+ exception: *mut JSValueRef,
98+ ) -> JSObjectRef;
99+ fn JSObjectIsFunction(context: JSContextRef, object: JSObjectRef) -> bool;
100+ fn JSObjectSetPropertyAtIndex(
101+ context: JSContextRef,
102+ object: JSObjectRef,
103+ index: u32,
104+ value: JSValueRef,
105+ exception: *mut JSValueRef,
106+ );
107+ fn JSObjectMakeFunctionWithCallback(
108+ context: JSContextRef,
109+ name: JSStringRef,
110+ callback: JSObjectCallAsFunctionCallback,
111+ ) -> JSObjectRef;
112+ fn JSObjectGetProperty(
113+ context: JSContextRef,
114+ object: JSObjectRef,
115+ name: JSStringRef,
116+ exception: *mut JSValueRef,
117+ ) -> JSValueRef;
118+ fn JSObjectSetProperty(
119+ context: JSContextRef,
120+ object: JSObjectRef,
121+ name: JSStringRef,
122+ value: JSValueRef,
123+ attributes: u32,
124+ exception: *mut JSValueRef,
125+ );
126+ fn JSObjectGetPropertyAtIndex(
127+ context: JSContextRef,
128+ object: JSObjectRef,
129+ index: u32,
130+ exception: *mut JSValueRef,
131+ ) -> JSValueRef;
132+ fn JSObjectCallAsFunction(
133+ context: JSContextRef,
134+ object: JSObjectRef,
135+ this_object: JSObjectRef,
136+ argument_count: usize,
137+ arguments: *const JSValueRef,
138+ exception: *mut JSValueRef,
139+ ) -> JSValueRef;
140+ fn JSValueGetType(context: JSContextRef, value: JSValueRef) -> i32;
141+ fn JSValueIsUndefined(context: JSContextRef, value: JSValueRef) -> bool;
142+ fn JSValueIsNull(context: JSContextRef, value: JSValueRef) -> bool;
143+ fn JSValueIsArray(context: JSContextRef, value: JSValueRef) -> bool;
144+ fn JSValueMakeUndefined(context: JSContextRef) -> JSValueRef;
145+ fn JSValueMakeBoolean(context: JSContextRef, value: bool) -> JSValueRef;
146+ fn JSValueMakeNumber(context: JSContextRef, value: f64) -> JSValueRef;
147+ fn JSValueMakeString(context: JSContextRef, value: JSStringRef) -> JSValueRef;
148+ fn JSValueProtect(context: JSContextRef, value: JSValueRef);
149+ fn JSValueUnprotect(context: JSContextRef, value: JSValueRef);
150+ fn JSValueToBoolean(context: JSContextRef, value: JSValueRef) -> bool;
151+ fn JSValueToNumber(context: JSContextRef, value: JSValueRef, exception: *mut JSValueRef)
152+ -> f64;
153+ fn JSValueToStringCopy(
154+ context: JSContextRef,
155+ value: JSValueRef,
156+ exception: *mut JSValueRef,
157+ ) -> JSStringRef;
158+ fn JSStringCreateWithCharacters(characters: *const u16, length: usize) -> JSStringRef;
159+ fn JSStringRelease(value: JSStringRef);
160+ fn JSStringGetMaximumUTF8CStringSize(value: JSStringRef) -> usize;
161+ fn JSStringGetUTF8CString(value: JSStringRef, buffer: *mut c_char, size: usize) -> usize;
162+}
163+
164+const JS_TYPE_BOOLEAN: i32 = 2;
165+const JS_TYPE_NUMBER: i32 = 3;
166+const JS_TYPE_STRING: i32 = 4;
167+const JS_TYPE_OBJECT: i32 = 5;
168+const WATCHDOG_PERIOD: f64 = 0.01;
169+
170+struct JsString(JSStringRef);
171+
172+impl JsString {
173+ fn new(value: &str) -> Result<Self, JsFailure> {
174+ let characters: Vec<u16> = value.encode_utf16().collect();
175+ let string = unsafe { JSStringCreateWithCharacters(characters.as_ptr(), characters.len()) };
176+ if string.is_null() {
177+ Err(JsFailure::error("could not allocate JavaScript string"))
178+ } else {
179+ Ok(Self(string))
180+ }
181+ }
182+
183+ fn copy_to_string(value: JSStringRef) -> String {
184+ unsafe {
185+ let size = JSStringGetMaximumUTF8CStringSize(value);
186+ let mut bytes = Vec::<u8>::with_capacity(size);
187+ let written = JSStringGetUTF8CString(value, bytes.as_mut_ptr().cast(), size);
188+ if written == 0 {
189+ return String::new();
190+ }
191+ bytes.set_len(written - 1);
192+ match String::from_utf8(bytes) {
193+ Ok(value) => value,
194+ Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
195+ }
196+ }
197+ }
198+}
199+
200+impl Drop for JsString {
201+ fn drop(&mut self) {
202+ unsafe { JSStringRelease(self.0) }
203+ }
204+}
205+
206+struct ProtectedValue {
207+ context: JSContextRef,
208+ value: JSValueRef,
209+}
210+
211+impl ProtectedValue {
212+ fn new(context: JSContextRef, value: JSValueRef) -> Self {
213+ unsafe { JSValueProtect(context, value) };
214+ Self { context, value }
215+ }
216+}
217+
218+impl Drop for ProtectedValue {
219+ fn drop(&mut self) {
220+ unsafe { JSValueUnprotect(self.context, self.value) };
221+ }
222+}
223+
224+enum JsFailure {
225+ Thrown(JSValueRef),
226+ Error(&'static str),
227+ Message(String),
228+ Type(&'static str),
229+ TypeMessage(String),
230+ Range(&'static str),
231+}
232+
233+impl JsFailure {
234+ fn error(message: &'static str) -> Self {
235+ Self::Error(message)
236+ }
237+
238+ fn native(error: Box<dyn Error>) -> Self {
239+ Self::Message(error.to_string())
240+ }
241+}
242+
243+struct Api {
244+ context: JSContextRef,
245+}
246+
247+impl Api {
248+ fn native<T>(&self, result: HostResult<T>) -> Result<T, JsFailure> {
249+ result.map_err(JsFailure::native)
250+ }
251+
252+ fn check_exception(&self, value: JSValueRef) -> Result<(), JsFailure> {
253+ if value.is_null() {
254+ Ok(())
255+ } else {
256+ Err(JsFailure::Thrown(value))
257+ }
258+ }
259+
260+ fn value_type(&self, value: JSValueRef) -> i32 {
261+ unsafe { JSValueGetType(self.context, value) }
262+ }
263+
264+ fn argument<'a>(&self, args: &'a [JSValueRef], index: usize) -> Result<JSValueRef, JsFailure> {
265+ args.get(index)
266+ .copied()
267+ .ok_or(JsFailure::Type("missing native argument"))
268+ }
269+
270+ fn string_value(&self, value: JSValueRef) -> Result<String, JsFailure> {
271+ if self.value_type(value) != JS_TYPE_STRING {
272+ return Err(JsFailure::Type("native argument must be a string"));
273+ }
274+ unsafe {
275+ let mut exception = ptr::null();
276+ let string_ref = JSValueToStringCopy(self.context, value, &mut exception);
277+ self.check_exception(exception)?;
278+ if string_ref.is_null() {
279+ return Err(JsFailure::error("could not read JavaScript string"));
280+ }
281+ let string = JsString::copy_to_string(string_ref);
282+ JSStringRelease(string_ref);
283+ Ok(string)
284+ }
285+ }
286+
287+ fn arg_string(&self, args: &[JSValueRef], index: usize) -> Result<String, JsFailure> {
288+ self.string_value(self.argument(args, index)?)
289+ }
290+
291+ fn optional_string(
292+ &self,
293+ args: &[JSValueRef],
294+ index: usize,
295+ ) -> Result<Option<String>, JsFailure> {
296+ let value = self.argument(args, index)?;
297+ if unsafe { JSValueIsUndefined(self.context, value) || JSValueIsNull(self.context, value) }
298+ {
299+ Ok(None)
300+ } else {
301+ self.string_value(value).map(Some)
302+ }
303+ }
304+
305+ fn boolean_value(&self, value: JSValueRef) -> Result<bool, JsFailure> {
306+ if self.value_type(value) != JS_TYPE_BOOLEAN {
307+ return Err(JsFailure::Type("native argument must be a boolean"));
308+ }
309+ Ok(unsafe { JSValueToBoolean(self.context, value) })
310+ }
311+
312+ fn boolean(&self, args: &[JSValueRef], index: usize) -> Result<bool, JsFailure> {
313+ self.boolean_value(self.argument(args, index)?)
314+ }
315+
316+ fn number_value(&self, value: JSValueRef) -> Result<f64, JsFailure> {
317+ if self.value_type(value) != JS_TYPE_NUMBER {
318+ return Err(JsFailure::Type("native argument must be a number"));
319+ }
320+ unsafe {
321+ let mut exception = ptr::null();
322+ let number = JSValueToNumber(self.context, value, &mut exception);
323+ self.check_exception(exception)?;
324+ Ok(number)
325+ }
326+ }
327+
328+ fn f32(&self, args: &[JSValueRef], index: usize) -> Result<f32, JsFailure> {
329+ Ok(self.number_value(self.argument(args, index)?)? as f32)
330+ }
331+
332+ fn i32(&self, args: &[JSValueRef], index: usize) -> Result<i32, JsFailure> {
333+ let number = self.number_value(self.argument(args, index)?)?;
334+ if number < i32::MIN as f64 || number > i32::MAX as f64 {
335+ return Err(JsFailure::Range("native integer argument is out of range"));
336+ }
337+ Ok(number as i32)
338+ }
339+
340+ fn u32(&self, args: &[JSValueRef], index: usize) -> Result<u32, JsFailure> {
341+ let number = self.number_value(self.argument(args, index)?)?;
342+ if number < 0.0 || number > u32::MAX as f64 {
343+ return Err(JsFailure::Range(
344+ "native unsigned integer argument is out of range",
345+ ));
346+ }
347+ Ok(number as u32)
348+ }
349+
350+ fn object_value(&self, value: JSValueRef) -> Result<JSObjectRef, JsFailure> {
351+ if self.value_type(value) != JS_TYPE_OBJECT {
352+ return Err(JsFailure::Type("native argument must be an object"));
353+ }
354+ Ok(value.cast_mut())
355+ }
356+
357+ fn object(&self, args: &[JSValueRef], index: usize) -> Result<JSObjectRef, JsFailure> {
358+ self.object_value(self.argument(args, index)?)
359+ }
360+
361+ fn optional_object(
362+ &self,
363+ args: &[JSValueRef],
364+ index: usize,
365+ ) -> Result<Option<JSObjectRef>, JsFailure> {
366+ let value = self.argument(args, index)?;
367+ if unsafe { JSValueIsUndefined(self.context, value) || JSValueIsNull(self.context, value) }
368+ {
369+ Ok(None)
370+ } else {
371+ self.object_value(value).map(Some)
372+ }
373+ }
374+
375+ fn property(&self, object: JSObjectRef, name: &str) -> Result<JSValueRef, JsFailure> {
376+ let name = JsString::new(name)?;
377+ unsafe {
378+ let mut exception = ptr::null();
379+ let value = JSObjectGetProperty(self.context, object, name.0, &mut exception);
380+ self.check_exception(exception)?;
381+ Ok(value)
382+ }
383+ }
384+
385+ fn property_string(&self, object: JSObjectRef, name: &str) -> Result<String, JsFailure> {
386+ self.string_value(self.property(object, name)?)
387+ }
388+
389+ fn property_f32(&self, object: JSObjectRef, name: &str) -> Result<f32, JsFailure> {
390+ Ok(self.number_value(self.property(object, name)?)? as f32)
391+ }
392+
393+ fn property_i32(&self, object: JSObjectRef, name: &str) -> Result<i32, JsFailure> {
394+ let value = self.number_value(self.property(object, name)?)?;
395+ if value < i32::MIN as f64 || value > i32::MAX as f64 {
396+ return Err(JsFailure::Range("native integer property is out of range"));
397+ }
398+ Ok(value as i32)
399+ }
400+
401+ fn property_u32(&self, object: JSObjectRef, name: &str) -> Result<u32, JsFailure> {
402+ let value = self.number_value(self.property(object, name)?)?;
403+ if value < 0.0 || value > u32::MAX as f64 {
404+ return Err(JsFailure::Range(
405+ "native unsigned integer property is out of range",
406+ ));
407+ }
408+ Ok(value as u32)
409+ }
410+
411+ fn strings(&self, args: &[JSValueRef], index: usize) -> Result<Vec<String>, JsFailure> {
412+ let value = self.argument(args, index)?;
413+ if !unsafe { JSValueIsArray(self.context, value) } {
414+ return Err(JsFailure::Type("native argument must be an array"));
415+ }
416+ let array = value.cast_mut();
417+ let length = self.property_u32(array, "length")?;
418+ let mut values = Vec::with_capacity(length as usize);
419+ for index in 0..length {
420+ unsafe {
421+ let mut exception = ptr::null();
422+ let value = JSObjectGetPropertyAtIndex(self.context, array, index, &mut exception);
423+ self.check_exception(exception)?;
424+ values.push(self.string_value(value)?);
425+ }
426+ }
427+ Ok(values)
428+ }
429+
430+ fn color(&self, object: JSObjectRef) -> Result<graphics::Color, JsFailure> {
431+ Ok([
432+ self.property_u32(object, "r")? as u8,
433+ self.property_u32(object, "g")? as u8,
434+ self.property_u32(object, "b")? as u8,
435+ self.property_u32(object, "a")? as u8,
436+ ])
437+ }
438+
439+ fn affine(&self, object: JSObjectRef) -> Result<[f32; 6], JsFailure> {
440+ Ok([
441+ self.property_f32(object, "a")?,
442+ self.property_f32(object, "b")?,
443+ self.property_f32(object, "c")?,
444+ self.property_f32(object, "d")?,
445+ self.property_f32(object, "e")?,
446+ self.property_f32(object, "f")?,
447+ ])
448+ }
449+
450+ fn stroke(&self, object: JSObjectRef) -> Result<graphics::Stroke, JsFailure> {
451+ Ok(graphics::Stroke {
452+ color: self.color(self.object_value(self.property(object, "color")?)?)?,
453+ width: self.property_f32(object, "width")?,
454+ join: self.property_i32(object, "join")?,
455+ cap: self.property_i32(object, "cap")?,
456+ })
457+ }
458+
459+ fn path(&self, object: JSObjectRef) -> Result<skia_safe::Path, JsFailure> {
460+ let values_value = self.property(object, "values")?;
461+ if !unsafe { JSValueIsArray(self.context, values_value) } {
462+ return Err(JsFailure::Type("canvas path values must be an array"));
463+ }
464+ let values = self.object_value(values_value)?;
465+ let offset = self.property_i32(object, "offset")?;
466+ let length = self.property_i32(object, "length")?;
467+ let array_length = self.property_u32(values, "length")?;
468+ let end = offset
469+ .checked_add(length)
470+ .filter(|end| offset >= 0 && length >= 0 && (*end as u32) <= array_length)
471+ .ok_or(JsFailure::Range("invalid canvas path range"))?;
472+ self.native(graphics::path_from_values((offset as u32..end as u32).map(
473+ |index| unsafe {
474+ let mut exception = ptr::null();
475+ let value = JSObjectGetPropertyAtIndex(self.context, values, index, &mut exception);
476+ self.check_exception(exception)
477+ .and_then(|()| self.number_value(value))
478+ .map_err(|error| failure(self.failure_message(error)))
479+ },
480+ )))
481+ }
482+
483+ fn failure_message(&self, error: JsFailure) -> String {
484+ match error {
485+ JsFailure::Thrown(value) => self.exception_string(value),
486+ JsFailure::Error(message) | JsFailure::Type(message) | JsFailure::Range(message) => {
487+ message.into()
488+ }
489+ JsFailure::Message(message) | JsFailure::TypeMessage(message) => message,
490+ }
491+ }
492+
493+ fn undefined(&self) -> JSValueRef {
494+ unsafe { JSValueMakeUndefined(self.context) }
495+ }
496+ fn bool(&self, value: bool) -> JSValueRef {
497+ unsafe { JSValueMakeBoolean(self.context, value) }
498+ }
499+ fn number(&self, value: impl Into<f64>) -> JSValueRef {
500+ unsafe { JSValueMakeNumber(self.context, value.into()) }
501+ }
502+
503+ fn string(&self, value: &str) -> Result<JSValueRef, JsFailure> {
504+ let value = JsString::new(value)?;
505+ Ok(unsafe { JSValueMakeString(self.context, value.0) })
506+ }
507+
508+ fn argument_array(&self, arguments: &[String]) -> Result<JSValueRef, JsFailure> {
509+ let mut exception = ptr::null();
510+ let array = unsafe { JSObjectMakeArray(self.context, 0, ptr::null(), &mut exception) };
511+ self.check_exception(exception)?;
512+ if array.is_null() {
513+ return Err(JsFailure::error(
514+ "could not allocate JavaScript argument array",
515+ ));
516+ }
517+ // Insert each value immediately into a rooted array rather than retain
518+ // unrooted JS strings in a Rust heap vector while subsequent strings allocate.
519+ let array_root = ProtectedValue::new(self.context, array.cast_const());
520+ for (index, value) in arguments.iter().enumerate() {
521+ let value = self.string(value)?;
522+ unsafe {
523+ JSObjectSetPropertyAtIndex(
524+ self.context,
525+ array,
526+ index as u32,
527+ value,
528+ &mut exception,
529+ );
530+ }
531+ self.check_exception(exception)?;
532+ }
533+ Ok(array_root.value)
534+ }
535+
536+ fn object_output(&self) -> JSObjectRef {
537+ unsafe { JSObjectMake(self.context, ptr::null(), ptr::null_mut()) }
538+ }
539+
540+ fn set(&self, object: JSObjectRef, name: &str, value: JSValueRef) -> Result<(), JsFailure> {
541+ let name = JsString::new(name)?;
542+ unsafe {
543+ let mut exception = ptr::null();
544+ JSObjectSetProperty(self.context, object, name.0, value, 0, &mut exception);
545+ self.check_exception(exception)
546+ }
547+ }
548+
549+ fn exception_string(&self, value: JSValueRef) -> String {
550+ let description = unsafe {
551+ let mut exception = ptr::null();
552+ let string = JSValueToStringCopy(self.context, value, &mut exception);
553+ if self.check_exception(exception).is_err() || string.is_null() {
554+ return "JavaScript exception (message could not be read)".into();
555+ }
556+ let result = JsString::copy_to_string(string);
557+ JSStringRelease(string);
558+ result
559+ };
560+ if self.value_type(value) == JS_TYPE_OBJECT {
561+ if let Ok(stack) = self.property(value.cast_mut(), "stack") {
562+ if let Ok(stack) = self.string_value(stack) {
563+ if !stack.is_empty() && !stack.starts_with(&description) {
564+ return format!("{description}\n{stack}");
565+ }
566+ }
567+ }
568+ }
569+ description
570+ }
571+
572+ fn make_exception(&self, error: JsFailure) -> JSValueRef {
573+ if let JsFailure::Thrown(value) = error {
574+ return value;
575+ }
576+ let (name, message) = match error {
577+ JsFailure::Error(message) => ("Error", message.into()),
578+ JsFailure::Message(message) => ("Error", message),
579+ JsFailure::Type(message) => ("TypeError", message.into()),
580+ JsFailure::TypeMessage(message) => ("TypeError", message),
581+ JsFailure::Range(message) => ("RangeError", message.into()),
582+ JsFailure::Thrown(_) => unreachable!(),
583+ };
584+ let script = format!("new {name}({})", js_string_literal(&message));
585+ let script = match JsString::new(&script) {
586+ Ok(value) => value,
587+ Err(_) => return self.undefined(),
588+ };
589+ unsafe {
590+ let mut exception = ptr::null();
591+ let value = JSEvaluateScript(
592+ self.context,
593+ script.0,
594+ ptr::null_mut(),
595+ ptr::null_mut(),
596+ 1,
597+ &mut exception,
598+ );
599+ if exception.is_null() {
600+ value
601+ } else {
602+ exception
603+ }
604+ }
605+ }
606+}
607+
608+fn js_string_literal(value: &str) -> String {
609+ let mut quoted = String::with_capacity(value.len() + 2);
610+ quoted.push('"');
611+ for character in value.chars() {
612+ match character {
613+ '\\' => quoted.push_str("\\\\"),
614+ '"' => quoted.push_str("\\\""),
615+ '\n' => quoted.push_str("\\n"),
616+ '\r' => quoted.push_str("\\r"),
617+ '\u{2028}' => quoted.push_str("\\u2028"),
618+ '\u{2029}' => quoted.push_str("\\u2029"),
619+ character if character.is_control() => {
620+ use std::fmt::Write;
621+ let _ = write!(quoted, "\\u{:04x}", character as u32);
622+ }
623+ character => quoted.push(character),
624+ }
625+ }
626+ quoted.push('"');
627+ quoted
628+}
629+
630+fn dispatch(
631+ api: &Api,
632+ host: &mut Host,
633+ name: &str,
634+ args: &[JSValueRef],
635+) -> Result<JSValueRef, JsFailure> {
636+ macro_rules! result {
637+ ($value:expr) => {
638+ api.native($value).map(|_| api.undefined())
639+ };
640+ }
641+ match name {
642+ "arguments" => api.argument_array(&host.config.arguments),
643+ "version" => api.string(&host.config.version),
644+ "isStdinTTY" => Ok(api.bool(io::stdin().is_terminal())),
645+ "readStdin" => api
646+ .native(host.read_stdin())
647+ .and_then(|value| api.string(&value)),
648+ "readLine" => match api.native(host.read_line())? {
649+ Some(value) => api.string(&value),
650+ None => Ok(api.undefined()),
651+ },
652+ "readText" => api
653+ .native(std::fs::read_to_string(api.arg_string(args, 0)?).map_err(Into::into))
654+ .and_then(|value| api.string(&value)),
655+ "writeText" => result!(
656+ std::fs::write(api.arg_string(args, 0)?, api.arg_string(args, 1)?).map_err(Into::into)
657+ ),
658+ "stdout" => result!(write_output(
659+ &mut io::stdout().lock(),
660+ &api.arg_string(args, 0)?
661+ )),
662+ "stderr" => result!(write_output(
663+ &mut io::stderr().lock(),
664+ &api.arg_string(args, 0)?
665+ )),
666+ "absolutePath" => api
667+ .native(absolute_path(&api.arg_string(args, 0)?))
668+ .and_then(|value| api.string(&value)),
669+ "environment" => api.string(&std::env::var(api.arg_string(args, 0)?).unwrap_or_default()),
670+ "monotonicMilliseconds" => Ok(api.number(host.started.elapsed().as_secs_f64() * 1000.0)),
671+ "platform" => api.string(std::env::consts::OS),
672+ "runApplicationTask" | "runApplication" => {
673+ let outcome = if name == "runApplicationTask" {
674+ api.native(
675+ host.application_task(api.strings(args, 0)?, api.optional_string(args, 1)?),
676+ )?
677+ } else {
678+ api.native(host.application(api.strings(args, 0)?, api.optional_string(args, 1)?))?
679+ };
680+ let value = api.object_output();
681+ api.set(value, "success", api.bool(outcome.success))?;
682+ api.set(value, "code", api.number(outcome.code))?;
683+ api.set(value, "stderr", api.string(&outcome.stderr)?)?;
684+ Ok(value.cast_const())
685+ }
686+ "measureText" => {
687+ let font = api.arg_string(args, 0)?;
688+ let text = api.arg_string(args, 1)?;
689+ let measured = api.native(
690+ host.graphics()
691+ .and_then(|graphics| graphics.measure(&font, &text)),
692+ )?;
693+ let value = api.object_output();
694+ api.set(value, "width", api.number(measured.width))?;
695+ api.set(value, "ascent", api.number(measured.ascent))?;
696+ api.set(value, "descent", api.number(measured.descent))?;
697+ Ok(value.cast_const())
698+ }
699+ "canvasCreate" => {
700+ let backend = api.arg_string(args, 0)?;
701+ let options = api.object(args, 1)?;
702+ let graphics = api.native(host.graphics())?;
703+ api.native(graphics.create(
704+ &backend,
705+ api.property_i32(options, "width")?,
706+ api.property_i32(options, "height")?,
707+ ))
708+ .map(|value| api.number(value))
709+ }
710+ "canvasRelease" => result!(api.native(host.graphics())?.release(api.u32(args, 0)?)),
711+ "canvasReset" => result!(api.native(host.graphics())?.reset(api.u32(args, 0)?)),
712+ "canvasClear" => {
713+ let id = api.u32(args, 0)?;
714+ let color = api.color(api.object(args, 1)?)?;
715+ result!(api.native(host.graphics())?.clear(id, color))
716+ }
717+ "canvasSave" => result!(api.native(host.graphics())?.save(api.u32(args, 0)?)),
718+ "canvasRestore" => result!(api.native(host.graphics())?.restore(api.u32(args, 0)?)),
719+ "canvasSetTransform" => {
720+ let id = api.u32(args, 0)?;
721+ let matrix = api.affine(api.object(args, 1)?)?;
722+ result!(api.native(host.graphics())?.set_transform(id, matrix))
723+ }
724+ "canvasTransform" => {
725+ let id = api.u32(args, 0)?;
726+ let matrix = api.affine(api.object(args, 1)?)?;
727+ result!(api.native(host.graphics())?.transform(id, matrix))
728+ }
729+ "canvasClip" => {
730+ let id = api.u32(args, 0)?;
731+ let path = api.path(api.object(args, 1)?)?;
732+ let even_odd = api.boolean(args, 2)?;
733+ result!(api.native(host.graphics())?.clip(id, &path, even_odd))
734+ }
735+ "canvasSetBlend" => {
736+ let id = api.u32(args, 0)?;
737+ let mode = api.arg_string(args, 1)?;
738+ result!(api.native(host.graphics())?.set_blend(id, &mode))
739+ }
740+ "canvasSetBlur" => {
741+ let id = api.u32(args, 0)?;
742+ let radius = api.f32(args, 1)?;
743+ result!(api.native(host.graphics())?.set_blur(id, radius))
744+ }
745+ "canvasSetTint" => {
746+ let id = api.u32(args, 0)?;
747+ let color = api
748+ .optional_object(args, 1)?
749+ .map(|value| api.color(value))
750+ .transpose()?;
751+ result!(api.native(host.graphics())?.set_tint(id, color))
752+ }
753+ "canvasFillPath" => {
754+ let id = api.u32(args, 0)?;
755+ let path = api.path(api.object(args, 1)?)?;
756+ let color = api.color(api.object(args, 2)?)?;
757+ result!(api.native(host.graphics())?.fill_path(id, &path, color))
758+ }
759+ "canvasStrokePath" => {
760+ let id = api.u32(args, 0)?;
761+ let path = api.path(api.object(args, 1)?)?;
762+ let stroke = api.stroke(api.object(args, 2)?)?;
763+ result!(api.native(host.graphics())?.stroke_path(id, &path, stroke))
764+ }
765+ "canvasFillStrokePath" => {
766+ let id = api.u32(args, 0)?;
767+ let path = api.path(api.object(args, 1)?)?;
768+ let color = api.color(api.object(args, 2)?)?;
769+ let stroke = api.stroke(api.object(args, 3)?)?;
770+ result!(api
771+ .native(host.graphics())?
772+ .fill_stroke_path(id, &path, color, stroke))
773+ }
774+ "canvasText" => {
775+ let id = api.u32(args, 0)?;
776+ let options = api.object(args, 1)?;
777+ let font = api.property_string(options, "font")?;
778+ let text = api.property_string(options, "content")?;
779+ let x = api.property_f32(options, "x")?;
780+ let y = api.property_f32(options, "y")?;
781+ let color = api.color(api.object_value(api.property(options, "color")?)?)?;
782+ let align = api.property_i32(options, "align")?;
783+ let baseline = api.property_i32(options, "baseline")?;
784+ result!(api
785+ .native(host.graphics())?
786+ .text(id, &font, &text, x, y, color, align, baseline))
787+ }
788+ "canvasCirclePattern" => {
789+ let id = api.u32(args, 0)?;
790+ let options = api.object(args, 1)?;
791+ let rect = api.object_value(api.property(options, "rect")?)?;
792+ let pattern = graphics::CirclePattern {
793+ rect: [
794+ api.property_f32(rect, "x")?,
795+ api.property_f32(rect, "y")?,
796+ api.property_f32(rect, "width")?,
797+ api.property_f32(rect, "height")?,
798+ ],
799+ background: api.color(api.object_value(api.property(options, "background")?)?)?,
800+ ink: api.color(api.object_value(api.property(options, "ink")?)?)?,
801+ tile: api.property_f32(options, "tile")?,
802+ radius: api.property_f32(options, "radius")?,
803+ origin: [
804+ api.property_f32(options, "originX")?,
805+ api.property_f32(options, "originY")?,
806+ ],
807+ };
808+ result!(api.native(host.graphics())?.circle_pattern(id, pattern))
809+ }
810+ "canvasSurface" => result!(api
811+ .native(host.graphics())?
812+ .surface(api.u32(args, 0)?, api.u32(args, 1)?)),
813+ "canvasWritePng" => {
814+ let id = api.u32(args, 0)?;
815+ let path = api.arg_string(args, 1)?;
816+ result!(api.native(host.graphics())?.png(id, &path))
817+ }
818+ "sharedFrameOpen" => result!(host.open_shared_frames()),
819+ "sharedFrameCapture" => api
820+ .native(host.capture_shared_frame(api.u32(args, 0)?))
821+ .map(|value| api.number(value)),
822+ "sharedFrameInfo" => {
823+ let id = api.u32(args, 0)?;
824+ let (name, width, height, byte_length) = api.native(host.shared_frame_info(id))?;
825+ let value = api.object_output();
826+ api.set(value, "id", api.number(id))?;
827+ api.set(
828+ value,
829+ "name",
830+ api.string(std::str::from_utf8(name).expect("shared-memory names are ASCII"))?,
831+ )?;
832+ api.set(value, "width", api.number(width))?;
833+ api.set(value, "height", api.number(height))?;
834+ api.set(value, "byteLength", api.number(byte_length))?;
835+ Ok(value.cast_const())
836+ }
837+ "sharedFrameConsumed" => api
838+ .native(host.shared_frame_consumed(api.u32(args, 0)?))
839+ .map(|value| api.bool(value)),
840+
841+ "sharedFrameRelease" => result!(host.release_shared_frame(api.u32(args, 0)?)),
842+ "sharedFrameClose" => result!(host.close_shared_frames()),
843+ "beginVideo" => {
844+ let options = api.object(args, 0)?;
845+ api.native(host.begin_video(
846+ api.property_string(options, "output")?,
847+ api.property_i32(options, "fps")?,
848+ api.property_i32(options, "width")?,
849+ api.property_i32(options, "height")?,
850+ ))
851+ .map(|value| api.number(value))
852+ }
853+ "writeVideoFrame" => result!(host.video_frame(api.u32(args, 0)?, api.u32(args, 1)?)),
854+ "finishVideo" => result!(host.finish_video(api.u32(args, 0)?)),
855+ "cancelVideo" => result!(host.cancel_video(api.u32(args, 0)?)),
856+ "enterTerminal" => result!(host.terminal.enter()),
857+ "restoreTerminal" => result!(host.terminal.restore()),
858+ "terminalSize" => {
859+ let (width, height) = if io::stdin().is_terminal() || io::stdout().is_terminal() {
860+ api.native(crossterm::terminal::size().map_err(Into::into))?
861+ } else {
862+ (0, 0)
863+ };
864+ let value = api.object_output();
865+ api.set(value, "width", api.number(width))?;
866+ api.set(value, "height", api.number(height))?;
867+ Ok(value.cast_const())
868+ }
869+ "terminalGraphicsInspectSize" => {
870+ let (columns, rows, width, height) = api.native(Terminal::inspect_size())?;
871+ let value = api.object_output();
872+ api.set(value, "columns", api.number(columns))?;
873+ api.set(value, "rows", api.number(rows))?;
874+ api.set(value, "width", api.number(width))?;
875+ api.set(value, "height", api.number(height))?;
876+ Ok(value.cast_const())
877+ }
878+ "terminalGraphicsOpen" => result!(host.open_terminal_graphics()),
879+ "terminalGraphicsSize" => {
880+ let (columns, rows, width, height) = api.native(host.terminal.graphics_size())?;
881+ let value = api.object_output();
882+ api.set(value, "columns", api.number(columns))?;
883+ api.set(value, "rows", api.number(rows))?;
884+ api.set(value, "width", api.number(width))?;
885+ api.set(value, "height", api.number(height))?;
886+ Ok(value.cast_const())
887+ }
888+ "terminalGraphicsPresent" => {
889+ result!(host.terminal_present(api.u32(args, 0)?, api.i32(args, 1)?, api.i32(args, 2)?))
890+ }
891+ "terminalGraphicsReadKey" | "readKey" => api
892+ .native(host.terminal.key())
893+ .and_then(|value| api.string(&value)),
894+ "terminalGraphicsClose" => result!(host.close_terminal_graphics()),
895+ "sleepMilliseconds" => result!(host.sleep(api.i32(args, 0)?)),
896+ _ => Err(JsFailure::TypeMessage(format!(
897+ "unknown native primitive: {name}"
898+ ))),
899+ }
900+}
901+
902+struct JscHost {
903+ state: Rc<RefCell<Host>>,
904+}
905+thread_local! { static HOSTS: RefCell<HashMap<usize, Rc<JscHost>>> = RefCell::new(HashMap::new()); }
906+
907+unsafe extern "C" fn native_call(
908+ context: JSContextRef,
909+ _function: JSObjectRef,
910+ _this: JSObjectRef,
911+ count: usize,
912+ values: *const JSValueRef,
913+ exception: *mut JSValueRef,
914+) -> JSValueRef {
915+ let api = Api { context };
916+ let result = catch_unwind(AssertUnwindSafe(|| {
917+ let host = HOSTS
918+ .with(|hosts| hosts.borrow().get(&(context as usize)).cloned())
919+ .ok_or(JsFailure::error("native host is unavailable"))?;
920+ if count == 0 {
921+ return Err(JsFailure::Type("missing native primitive name"));
922+ }
923+ let values = unsafe { std::slice::from_raw_parts(values, count) };
924+ let name = api.string_value(values[0])?;
925+ let mut host = host
926+ .state
927+ .try_borrow_mut()
928+ .map_err(|_| JsFailure::error("native host cannot be called recursively"))?;
929+ dispatch(&api, &mut host, &name, &values[1..])
930+ }));
931+ match result {
932+ Ok(Ok(value)) => value,
933+ Ok(Err(error)) => {
934+ unsafe {
935+ *exception = api.make_exception(error);
936+ }
937+ api.undefined()
938+ }
939+ Err(_) => {
940+ unsafe {
941+ *exception = api.make_exception(JsFailure::error("native host panicked"));
942+ }
943+ api.undefined()
944+ }
945+ }
946+}
947+
948+unsafe extern "C" fn should_terminate(context: JSContextRef, data: *mut c_void) -> bool {
949+ let interrupted = unsafe { (&*(data as *const AtomicBool)).load(Ordering::Relaxed) };
950+ if !interrupted {
951+ // Keep polling after a non-terminating callback, including on WebKit
952+ // versions that do not automatically restart the elapsed timer.
953+ unsafe {
954+ JSContextGroupSetExecutionTimeLimit(
955+ JSContextGetGroup(context),
956+ WATCHDOG_PERIOD,
957+ Some(should_terminate),
958+ data,
959+ );
960+ }
961+ }
962+ interrupted
963+}
964+
965+struct Context {
966+ group: JSContextGroupRef,
967+ context: JSGlobalContextRef,
968+ state: Rc<RefCell<Host>>,
969+}
970+
971+impl Context {
972+ fn new(state: Rc<RefCell<Host>>) -> HostResult<Self> {
973+ let group = unsafe { JSContextGroupCreate() };
974+ if group.is_null() {
975+ return Err(failure("could not create JavaScriptCore context group"));
976+ }
977+ let context = unsafe { JSGlobalContextCreateInGroup(group, ptr::null()) };
978+ if context.is_null() {
979+ unsafe { JSContextGroupRelease(group) };
980+ return Err(failure("could not create JavaScriptCore context"));
981+ }
982+ unsafe {
983+ JSContextGroupSetExecutionTimeLimit(
984+ group,
985+ WATCHDOG_PERIOD,
986+ Some(should_terminate),
987+ Arc::as_ptr(&state.borrow().interrupted).cast_mut().cast(),
988+ );
989+ }
990+ HOSTS.with(|hosts| {
991+ hosts.borrow_mut().insert(
992+ context as usize,
993+ Rc::new(JscHost {
994+ state: state.clone(),
995+ }),
996+ );
997+ });
998+ Ok(Self {
999+ group,
1000+ context,
1001+ state,
1002+ })
1003+ }
1004+
1005+ fn close(&mut self) -> HostResult<()> {
1006+ if self.context.is_null() {
1007+ return Ok(());
1008+ }
1009+ HOSTS.with(|hosts| {
1010+ hosts.borrow_mut().remove(&(self.context as usize));
1011+ });
1012+ unsafe { JSContextGroupClearExecutionTimeLimit(self.group) };
1013+ let cleanup = match self.state.try_borrow_mut() {
1014+ Ok(mut state) => state.shutdown(),
1015+ Err(_) => Err(failure("native host is still borrowed during shutdown")),
1016+ };
1017+ unsafe {
1018+ JSGlobalContextRelease(self.context);
1019+ JSContextGroupRelease(self.group);
1020+ }
1021+ self.context = ptr::null_mut();
1022+ self.group = ptr::null();
1023+ cleanup
1024+ }
1025+}
1026+
1027+impl Drop for Context {
1028+ fn drop(&mut self) {
1029+ let _ = self.close();
1030+ }
1031+}
1032+
1033+fn execute(api: &Api, script: &str) -> Result<i32, JsFailure> {
1034+ let global = unsafe { JSContextGetGlobalObject(api.context) };
1035+ let call_name = JsString::new("call")?;
1036+ let host = api.object_output();
1037+ let call = unsafe { JSObjectMakeFunctionWithCallback(api.context, call_name.0, native_call) };
1038+ api.set(host, "call", call.cast_const())?;
1039+ api.set(global, "__purescriptNativeHost", host.cast_const())?;
1040+ let source = JsString::new(script)?;
1041+ let mut exception = ptr::null();
1042+ unsafe {
1043+ JSEvaluateScript(
1044+ api.context,
1045+ source.0,
1046+ ptr::null_mut(),
1047+ ptr::null_mut(),
1048+ 1,
1049+ &mut exception,
1050+ );
1051+ }
1052+ api.check_exception(exception)?;
1053+ let main = api.property(global, "__purescriptMain")?;
1054+ let main = api.object_value(main)?;
1055+ if !unsafe { JSObjectIsFunction(api.context, main) } {
1056+ return Err(JsFailure::Type("__purescriptMain must be a function"));
1057+ }
1058+ let status = unsafe {
1059+ JSObjectCallAsFunction(
1060+ api.context,
1061+ main,
1062+ ptr::null_mut(),
1063+ 0,
1064+ ptr::null(),
1065+ &mut exception,
1066+ )
1067+ };
1068+ api.check_exception(exception)?;
1069+ let status = api.number_value(status)?;
1070+ if !status.is_finite()
1071+ || status.fract() != 0.0
1072+ || status < i32::MIN as f64
1073+ || status > i32::MAX as f64
1074+ {
1075+ return Err(JsFailure::Type(
1076+ "__purescriptMain must return an integer exit status",
1077+ ));
1078+ }
1079+ Ok(status as i32)
1080+}
1081+
1082+pub(super) fn run(script: &str, config: Config) -> Result<i32, Box<dyn Error>> {
1083+ let _guard = RUN_LOCK
1084+ .lock()
1085+ .map_err(|_| failure("native runtime lock is poisoned"))?;
1086+ let interrupted = INTERRUPT
1087+ .as_ref()
1088+ .map_err(|error| failure(error.clone()))?
1089+ .clone();
1090+ interrupted.store(false, Ordering::Relaxed);
1091+ let state = Rc::new(RefCell::new(Host {
1092+ config,
1093+ graphics: None,
1094+ videos: HashMap::new(),
1095+ next_video: 1,
1096+ frames: HashMap::new(),
1097+ next_frame: 1,
1098+ shared_frames_active: false,
1099+ terminal: Default::default(),
1100+ stdin: io::BufReader::new(io::stdin()),
1101+ started: Instant::now(),
1102+ interrupted: interrupted.clone(),
1103+ }));
1104+ let mut context = Context::new(state)?;
1105+ let api = Api {
1106+ context: context.context.cast_const(),
1107+ };
1108+ let outcome = execute(&api, script).map_err(|error| failure(api.failure_message(error)));
1109+ let cleanup = context.close();
1110+ cleanup?;
1111+ if interrupted.load(Ordering::Relaxed) {
1112+ Ok(130)
1113+ } else {
1114+ outcome
1115+ }
1116+}
1117+
1118+#[cfg(test)]
1119+mod tests {
1120+ use super::run;
1121+ use crate::Config;
1122+ use std::path::PathBuf;
1123+ use std::sync::atomic::{AtomicU64, Ordering};
1124+
1125+ fn config(arguments: Vec<String>) -> Config {
1126+ Config {
1127+ arguments,
1128+ version: "test".into(),
1129+ fonts: Vec::new(),
1130+ application: PathBuf::new(),
1131+ ffmpeg: PathBuf::new(),
1132+ }
1133+ }
1134+
1135+ #[test]
1136+ fn unicode_and_nul_survive_script_arguments_and_native_file_io() {
1137+ static NEXT: AtomicU64 = AtomicU64::new(0);
1138+ let path = std::env::temp_dir().join(format!(
1139+ "tauri-jsc-🦀-{}-{}.txt",
1140+ std::process::id(),
1141+ NEXT.fetch_add(1, Ordering::Relaxed)
1142+ ));
1143+ let result = run(
1144+ r#"
1145+ globalThis.__purescriptMain = () => {
1146+ const host = globalThis.__purescriptNativeHost;
1147+ const path = host.call("arguments")[0];
1148+ const text = "before\u0000🦀 after";
1149+ host.call("writeText", path, text);
1150+ return host.call("readText", path) === text ? 19 : 1;
1151+ };
1152+ "#,
1153+ config(vec![path.to_string_lossy().into_owned()]),
1154+ );
1155+ let contents = std::fs::read_to_string(&path);
1156+ let _ = std::fs::remove_file(&path);
1157+ assert_eq!(result.unwrap(), 19);
1158+ assert_eq!(contents.unwrap(), "before\0🦀 after");
1159+ }
1160+
1161+ #[test]
1162+ fn cancellation_remains_armed_after_earlier_watchdog_periods() {
1163+ let path =
1164+ std::env::temp_dir().join(format!("tauri-jsc-cancel-{}.txt", std::process::id()));
1165+ let ready = path.clone();
1166+ let stopper = std::thread::spawn(move || {
1167+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
1168+ while !ready.exists() {
1169+ assert!(std::time::Instant::now() < deadline, "script never started");
1170+ std::thread::sleep(std::time::Duration::from_millis(1));
1171+ }
1172+ std::thread::sleep(std::time::Duration::from_millis(100));
1173+ let signalled = std::time::Instant::now();
1174+ crate::INTERRUPT
1175+ .as_ref()
1176+ .unwrap()
1177+ .store(true, Ordering::Relaxed);
1178+ signalled
1179+ });
1180+ let result = run(
1181+ r#"
1182+ globalThis.__purescriptMain = () => {
1183+ const host = globalThis.__purescriptNativeHost;
1184+ host.call("writeText", host.call("arguments")[0], "ready");
1185+ const end = Date.now() + 2000;
1186+ while (Date.now() < end) {}
1187+ return 0;
1188+ };
1189+ "#,
1190+ config(vec![path.to_string_lossy().into_owned()]),
1191+ );
1192+ let latency = stopper.join().unwrap().elapsed();
1193+ let _ = std::fs::remove_file(path);
1194+ assert_eq!(result.unwrap(), 130);
1195+ assert!(
1196+ latency < std::time::Duration::from_millis(500),
1197+ "cancellation took {latency:?}"
1198+ );
1199+ }
1200+
1201+ #[test]
1202+ fn argument_arrays_keep_all_values_alive_during_allocation() {
1203+ let arguments = (0..20_000)
1204+ .map(|index| format!("{index}:{}\0🦀", "label".repeat(32)))
1205+ .collect();
1206+ let status = run(
1207+ r#"
1208+ globalThis.__purescriptMain = () => {
1209+ const args = globalThis.__purescriptNativeHost.call("arguments");
1210+ if (args.length !== 20000) return 1;
1211+ for (let i = 0; i < args.length; ++i) {
1212+ if (args[i] !== i + ":" + "label".repeat(32) + "\u0000🦀") return 2;
1213+ }
1214+ return 23;
1215+ };
1216+ "#,
1217+ config(arguments),
1218+ )
1219+ .unwrap();
1220+ assert_eq!(status, 23);
1221+ }
1222+
1223+ #[test]
1224+ fn invalid_entry_points_and_exit_statuses_do_not_poison_later_runs() {
1225+ for body in [
1226+ "42",
1227+ "{}",
1228+ "() => NaN",
1229+ "() => Infinity",
1230+ "() => 0.5",
1231+ "() => 2147483648",
1232+ "() => -2147483649",
1233+ ] {
1234+ let source = format!("globalThis.__purescriptMain = {body};");
1235+ assert!(run(&source, config(Vec::new())).is_err(), "{body}");
1236+ }
1237+ assert_eq!(
1238+ run("globalThis.__purescriptMain = () => 37", config(Vec::new())).unwrap(),
1239+ 37
1240+ );
1241+ assert_eq!(
1242+ run(
1243+ "globalThis.__purescriptMain = () => -2147483648",
1244+ config(Vec::new())
1245+ )
1246+ .unwrap(),
1247+ i32::MIN
1248+ );
1249+ assert_eq!(
1250+ run(
1251+ "globalThis.__purescriptMain = () => 2147483647",
1252+ config(Vec::new())
1253+ )
1254+ .unwrap(),
1255+ i32::MAX
1256+ );
1257+ }
1258+
1259+ #[test]
1260+ fn native_errors_keep_types_messages_and_a_usable_canvas() {
1261+ let status = run(
1262+ r#"
1263+ globalThis.__purescriptMain = () => {
1264+ const host = globalThis.__purescriptNativeHost;
1265+ try { host.call("missingPrimitive"); return 1; }
1266+ catch (error) { if (!(error instanceof TypeError)) return 2; }
1267+ const canvas = host.call("canvasCreate", "cpu", {width:4,height:4});
1268+ try {
1269+ host.call("canvasFillPath", canvas,
1270+ {values:[0,0,0],offset:-1,length:3}, {r:0,g:0,b:0,a:255});
1271+ return 3;
1272+ } catch (error) { if (!(error instanceof RangeError)) return 4; }
1273+ try { host.call("canvasClear", canvas, {r:"invalid",g:0,b:0,a:255}); return 5; }
1274+ catch (error) { if (!(error instanceof TypeError)) return 6; }
1275+ host.call("canvasClear", canvas, {r:12,g:34,b:56,a:255});
1276+ host.call("canvasRelease", canvas);
1277+ return 41;
1278+ };
1279+ "#,
1280+ config(Vec::new()),
1281+ )
1282+ .unwrap();
1283+ assert_eq!(status, 41);
1284+ let error = run(
1285+ r#"
1286+ globalThis.__purescriptMain = () => { throw new Error("renderer cause 🦀"); };
1287+ "#,
1288+ config(Vec::new()),
1289+ )
1290+ .unwrap_err();
1291+ assert!(error.to_string().contains("renderer cause 🦀"), "{error}");
1292+ }
1293+}