ion/class/
mod.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::any::TypeId;
8use std::collections::hash_map::Entry;
9use std::ffi::CStr;
10use std::ptr;
11
12use ion_proc::Traceable;
13use mozjs::error::throw_type_error;
14use mozjs::gc::{HandleObject, Traceable};
15use mozjs::glue::JS_GetReservedSlot;
16use mozjs::jsapi::{
17	GCContext, Heap, JS_GetConstructor, JS_HasInstance, JS_InitClass, JS_InstanceOf, JS_NewObjectWithGivenProto,
18	JS_SetReservedSlot, JSContext, JSFunction, JSFunctionSpec, JSObject, JSPropertySpec, JSTracer,
19};
20use mozjs::jsval::{JSVal, NullValue, PrivateValue, UndefinedValue};
21use mozjs::rust::get_object_class;
22
23pub use crate::class::native::{MAX_PROTO_CHAIN_LENGTH, NativeClass, PrototypeChain, TypeIdWrapper};
24pub use crate::class::reflect::{Castable, DerivedFrom, NativeObject, Reflector};
25use crate::conversions::{IntoValue, ToValue as _};
26use crate::function::{__handle_native_constructor_result, NativeFunction};
27use crate::{
28	Arguments, Context, Error, ErrorKind, Exception, Function, Local, Object, Result, ResultExc, Value,
29	class_num_reserved_slots,
30};
31
32mod native;
33mod reflect;
34
35/// Stores information about a native class created for JS.
36#[derive(Debug, Traceable)]
37#[trace(crate = crate)]
38pub struct ClassInfo {
39	#[trace(no_trace)]
40	class: &'static NativeClass,
41	pub constructor: Box<Heap<*mut JSFunction>>,
42	pub prototype: Box<Heap<*mut JSObject>>,
43}
44
45pub trait ClassDefinition: NativeObject {
46	fn class() -> &'static NativeClass;
47
48	fn proto_class() -> Option<&'static NativeClass> {
49		None
50	}
51
52	fn parent_prototype(_: &Context) -> Option<Local<'_, *mut JSObject>> {
53		None
54	}
55
56	fn constructor() -> (Option<NativeFunction>, u32);
57
58	fn functions() -> Option<&'static [JSFunctionSpec]> {
59		None
60	}
61
62	fn properties() -> Option<&'static [JSPropertySpec]> {
63		None
64	}
65
66	fn static_functions() -> Option<&'static [JSFunctionSpec]> {
67		None
68	}
69
70	fn static_properties() -> Option<&'static [JSPropertySpec]> {
71		None
72	}
73
74	fn init_class<'cx>(cx: &'cx Context, object: &Object) -> (bool, &'cx ClassInfo) {
75		let infos = unsafe { &mut (*cx.get_inner_data().as_ptr()).class_infos };
76
77		match infos.entry(TypeId::of::<Self>()) {
78			Entry::Occupied(o) => (false, o.into_mut()),
79			Entry::Vacant(entry) => {
80				let proto_class = Self::proto_class().map_or_else(ptr::null, |class| &raw const class.base);
81				let parent_proto = Self::parent_prototype(cx).map_or_else(HandleObject::null, |proto| proto.handle());
82
83				let (constructor, nargs) = Self::constructor();
84
85				let properties = Self::properties();
86				let functions = Self::functions();
87				let static_properties = Self::static_properties();
88				let static_functions = Self::static_functions();
89
90				assert!(has_zero_spec(properties));
91				assert!(has_zero_spec(functions));
92				assert!(has_zero_spec(static_properties));
93				assert!(has_zero_spec(static_functions));
94
95				let class = unsafe {
96					JS_InitClass(
97						cx.as_ptr(),
98						object.handle().into(),
99						proto_class,
100						parent_proto.into(),
101						Self::class().base.name,
102						Some(constructor.unwrap_or(illegal_constructor)),
103						nargs,
104						unwrap_specs(properties),
105						unwrap_specs(functions),
106						unwrap_specs(static_properties),
107						unwrap_specs(static_functions),
108					)
109				};
110				let prototype = cx.root(class);
111
112				let constructor = unsafe { JS_GetConstructor(cx.as_ptr(), prototype.handle().into()) };
113				let constructor = Object::from(cx.root(constructor));
114				let constructor = Function::from_object(cx, &constructor).unwrap();
115
116				let class_info = ClassInfo {
117					class: Self::class(),
118					constructor: Heap::boxed(constructor.get()),
119					prototype: Heap::boxed(prototype.get()),
120				};
121
122				(true, entry.insert(class_info))
123			}
124		}
125	}
126
127	fn new_raw_object(cx: &Context) -> *mut JSObject {
128		let infos = unsafe { &mut (*cx.get_inner_data().as_ptr()).class_infos };
129		let info = infos.get(&TypeId::of::<Self>()).expect("Uninitialised Class");
130		unsafe {
131			JS_NewObjectWithGivenProto(
132				cx.as_ptr(),
133				&raw const Self::class().base,
134				Local::from_heap(&info.prototype).handle().into(),
135			)
136		}
137	}
138
139	fn new_object(cx: &Context, native: Box<Self>) -> *mut JSObject {
140		let object = Self::new_raw_object(cx);
141		unsafe {
142			Self::set_private(object, native);
143		}
144		object
145	}
146
147	unsafe fn get_private_unchecked<'a>(object: &Object<'a>) -> &'a Self {
148		unsafe {
149			let mut value = UndefinedValue();
150			JS_GetReservedSlot(object.handle().get(), 0, &raw mut value);
151			&*(value.to_private().cast::<Self>())
152		}
153	}
154
155	fn get_private<'a>(cx: &Context, object: &Object<'a>) -> Result<&'a Self> {
156		check_private::<Self>(cx, object)?;
157		Ok(unsafe { Self::get_private_unchecked(object) })
158	}
159
160	unsafe fn get_mut_private_unchecked<'a>(object: &Object<'a>) -> &'a mut Self {
161		unsafe {
162			let mut value = UndefinedValue();
163			JS_GetReservedSlot(object.handle().get(), 0, &raw mut value);
164			&mut *(value.to_private().cast_mut().cast::<Self>())
165		}
166	}
167
168	fn get_mut_private<'a>(cx: &Context, object: &Object<'a>) -> Result<&'a mut Self> {
169		check_private::<Self>(cx, object)?;
170		Ok(unsafe { Self::get_mut_private_unchecked(object) })
171	}
172
173	unsafe fn set_private(object: *mut JSObject, native: Box<Self>) {
174		native.reflector().set(object);
175		unsafe {
176			JS_SetReservedSlot(object, 0, &PrivateValue(Box::into_raw(native).cast_const().cast()));
177		}
178	}
179
180	fn instance_of(cx: &Context, object: &Object) -> bool {
181		unsafe {
182			JS_InstanceOf(
183				cx.as_ptr(),
184				object.handle().into(),
185				&raw const Self::class().base,
186				ptr::null_mut(),
187			)
188		}
189	}
190
191	fn has_instance(cx: &Context, object: &Object) -> Result<bool> {
192		let infos = unsafe { &mut (*cx.get_inner_data().as_ptr()).class_infos };
193		let constructor =
194			Function::from(cx.root(infos.get(&TypeId::of::<Self>()).expect("Uninitialised Class").constructor.get()))
195				.to_object(cx);
196		let object = object.as_value(cx);
197		let mut has_instance = false;
198		let result = unsafe {
199			JS_HasInstance(
200				cx.as_ptr(),
201				constructor.handle().into(),
202				object.handle().into(),
203				&raw mut has_instance,
204			)
205		};
206		if result { Ok(has_instance) } else { Err(Error::none()) }
207	}
208}
209
210pub struct ClassObjectWrapper<T: ClassDefinition>(pub Box<T>);
211
212impl<T: ClassDefinition> IntoValue<'_> for ClassObjectWrapper<T> {
213	fn into_value(self: Box<Self>, cx: &Context, value: &mut Value) {
214		T::new_object(cx, self.0).to_value(cx, value);
215	}
216}
217
218trait SpecZero {
219	fn is_zeroed(&self) -> bool;
220}
221
222impl SpecZero for JSFunctionSpec {
223	fn is_zeroed(&self) -> bool {
224		self.is_zeroed()
225	}
226}
227
228impl SpecZero for JSPropertySpec {
229	fn is_zeroed(&self) -> bool {
230		self.is_zeroed()
231	}
232}
233
234fn has_zero_spec<T: SpecZero>(specs: Option<&[T]>) -> bool {
235	specs.and_then(|s| s.last()).is_none_or(T::is_zeroed)
236}
237
238fn unwrap_specs<T>(specs: Option<&[T]>) -> *const T {
239	specs.map_or_else(ptr::null, <[T]>::as_ptr)
240}
241
242fn check_private<T: ClassDefinition>(cx: &Context, object: &Object) -> Result<()> {
243	if unsafe { class_num_reserved_slots(get_object_class(object.handle().get())) >= 1 }
244		&& (T::instance_of(cx, object) || T::has_instance(cx, object)?)
245	{
246		Ok(())
247	} else {
248		let name = unsafe { CStr::from_ptr(T::class().base.name).to_str()? };
249		Err(Error::new(
250			format!("Object does not implement interface {name}"),
251			ErrorKind::Type,
252		))
253	}
254}
255
256unsafe extern "C" fn illegal_constructor(cx: *mut JSContext, _: u32, _: *mut JSVal) -> bool {
257	unsafe {
258		throw_type_error(cx, "Illegal constructor.");
259	}
260	false
261}
262
263#[doc(hidden)]
264pub fn construct_native_object<'cx, T, C>(cx: &'cx Context, args: &mut Arguments<'cx>, constructor: C) -> bool
265where
266	T: ClassDefinition,
267	C: for<'cx2> FnOnce(&'cx2 Context, &mut Arguments<'cx2>, &mut Object<'cx2>) -> ResultExc<()>,
268{
269	if !args.is_constructing() {
270		debug_assert!(!Exception::is_pending(cx));
271		unsafe {
272			let name = CStr::from_ptr(T::class().base.name).to_str().unwrap();
273			throw_type_error(cx.as_ptr(), &format!("{name} constructor: 'new' is required"));
274		}
275		return false;
276	}
277
278	let class_info = T::init_class(cx, &Object::global(cx)).1;
279
280	let mut this = Object::from(cx.root(unsafe {
281		JS_NewObjectWithGivenProto(cx.as_ptr(), &raw const T::class().base, class_info.prototype.handle())
282	}));
283
284	let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| constructor(cx, args, &mut this)));
285
286	__handle_native_constructor_result(cx, result, &this, &mut args.rval())
287}
288
289#[doc(hidden)]
290pub unsafe extern "C" fn finalise_native_object_operation<T>(_: *mut GCContext, this: *mut JSObject) {
291	let mut value = NullValue();
292	unsafe {
293		JS_GetReservedSlot(this, 0, &raw mut value);
294	}
295	if value.is_double() && value.asBits_ & 0xFFFF000000000000 == 0 {
296		let private = value.to_private().cast_mut().cast::<T>();
297		let _ = unsafe { Box::from_raw(private) };
298	}
299}
300
301#[doc(hidden)]
302pub unsafe extern "C" fn trace_native_object_operation<T: Traceable>(trc: *mut JSTracer, this: *mut JSObject) {
303	let mut value = NullValue();
304	unsafe {
305		JS_GetReservedSlot(this, 0, &raw mut value);
306	}
307	if value.is_double() && value.asBits_ & 0xFFFF000000000000 == 0 {
308		unsafe {
309			let private = &*(value.to_private().cast::<T>());
310			private.trace(trc);
311		}
312	}
313}