Skip to content

Commit 9e1a210

Browse files
committed
improve shape implement
1 parent a64e6f9 commit 9e1a210

File tree

2 files changed

+180
-51
lines changed

2 files changed

+180
-51
lines changed

src/core.rs

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ pub struct JSShape {
277277
pub deleted_prop_count: i32,
278278
pub prop: *mut JSShapeProperty,
279279
pub prop_hash: *mut u32,
280+
// Linked list of objects which currently use this shape. Each object's
281+
// `next_in_shape` pointer links to the next object in the list.
282+
pub first_object: *mut JSObject,
280283
pub proto: *mut JSObject,
281284
}
282285

@@ -394,6 +397,8 @@ pub struct JSObject {
394397
pub shape: *mut JSShape,
395398
pub prop: *mut JSProperty,
396399
pub first_weak_ref: *mut JSObject,
400+
// Pointer to next object in the shape's object list
401+
pub next_in_shape: *mut JSObject,
397402
}
398403

399404
#[repr(C)]
@@ -471,9 +476,47 @@ impl JSRuntime {
471476

472477
if (*sh).prop_count >= (*sh).prop_size {
473478
let new_size = if (*sh).prop_size == 0 { 4 } else { (*sh).prop_size * 3 / 2 };
479+
// Resize the shape's property descriptor array
474480
if self.resize_shape(sh, new_size) < 0 {
475481
return (-1, prev_prop_size);
476482
}
483+
484+
// For all objects that currently use this shape, reallocate their
485+
// JSProperty arrays to match the new shape size so every object
486+
// has space for the newly added property slots.
487+
let mut obj_ptr = (*sh).first_object;
488+
while !obj_ptr.is_null() {
489+
// keep next pointer in case reallocation moves memory
490+
let next_obj = (*obj_ptr).next_in_shape;
491+
492+
let old_prop = (*obj_ptr).prop;
493+
let new_prop_obj = self.js_realloc_rt(old_prop as *mut c_void, (new_size as usize) * std::mem::size_of::<JSProperty>())
494+
as *mut JSProperty;
495+
496+
if new_prop_obj.is_null() {
497+
return (-1, prev_prop_size);
498+
}
499+
500+
// If we just allocated the array, zero-initialize all slots.
501+
if old_prop.is_null() {
502+
// Initialize all slots to JS_UNDEFINED (correct default), avoid raw zeros
503+
for i in 0..(new_size as isize) {
504+
(*new_prop_obj.offset(i)).u.value = JS_UNDEFINED;
505+
}
506+
} else if new_size > prev_prop_size {
507+
// Zero only the newly-added slots
508+
let start_index = prev_prop_size as usize;
509+
let new_slots = (new_size - prev_prop_size) as usize;
510+
if new_slots > 0 {
511+
for i in start_index..(start_index + new_slots) {
512+
(*new_prop_obj.add(i)).u.value = JS_UNDEFINED;
513+
}
514+
}
515+
}
516+
517+
(*obj_ptr).prop = new_prop_obj;
518+
obj_ptr = next_obj;
519+
}
477520
}
478521

479522
// Enable hash if needed
@@ -606,6 +649,7 @@ impl JSRuntime {
606649
(*sh).deleted_prop_count = 0;
607650
(*sh).prop = std::ptr::null_mut();
608651
(*sh).prop_hash = std::ptr::null_mut();
652+
(*sh).first_object = std::ptr::null_mut();
609653
(*sh).proto = proto;
610654
sh
611655
}
@@ -642,60 +686,30 @@ pub unsafe fn JS_DefinePropertyValue(ctx: *mut JSContext, this_obj: JSValue, pro
642686
// Note: In real QuickJS, we might need to clone shape if it is shared
643687
// For now, assume shape is unique to object or we modify it in place (dangerous if shared)
644688

645-
let (idx, prev_prop_size) = unsafe { (*(*ctx).rt).add_property(sh, prop, flags as u8) };
689+
let (idx, _prev_prop_size) = unsafe { (*(*ctx).rt).add_property(sh, prop, flags as u8) };
646690
if idx < 0 {
647691
return -1;
648692
}
649693

650-
// Resize object prop array if needed
651-
// JSObject prop array stores JSProperty (values)
652-
// JSShape prop array stores JSShapeProperty (names/flags)
653-
// They must match in size/index
654-
655-
// TODO: Resize object prop array
656-
// For now, let's assume we have enough space or implement resize logic for object prop
657-
658-
// Actually, we need to implement object prop resizing here
659-
// But JSObject definition: pub prop: *mut JSProperty
660-
// We don't store prop_size in JSObject?
661-
// QuickJS stores it in JSShape? No.
662-
// QuickJS: JSObject has no size field. It relies on Shape?
663-
// Ah, JSObject allocates prop array based on shape->prop_size?
664-
// Or maybe it reallocates when shape grows?
665-
666-
// Let's look at QuickJS:
667-
// JS_DefinePropertyValue -> JS_DefineProperty -> add_property
668-
// add_property modifies shape.
669-
// If shape grows, we need to grow object's prop array too?
670-
// Yes, but how do we know the current size of object's prop array?
671-
// It seems we assume it matches shape's prop_count or prop_size?
672-
673-
// Let's implement a simple resize for object prop
694+
// After add_property we should have ensured the shape has enough capacity and
695+
// that objects using this shape have their prop arrays resized. However it's
696+
// still possible this particular object has no prop array (for example a
697+
// newly created object that was not present when the shape grew). In that
698+
// case allocate a prop array sized to the current shape.
674699
let old_prop = unsafe { (*p).prop };
675-
let new_prop = unsafe {
676-
(*(*ctx).rt).js_realloc_rt(
677-
(*p).prop as *mut c_void,
678-
((*sh).prop_size as usize) * std::mem::size_of::<JSProperty>(),
679-
) as *mut JSProperty
680-
};
681-
682-
if new_prop.is_null() {
683-
return -1;
684-
}
685-
unsafe { (*p).prop = new_prop };
686-
// If the prop array was just created, zero-initialize it to avoid reading
687-
// uninitialized JSProperty values later.
688-
if old_prop.is_null() && !new_prop.is_null() {
689-
let size_bytes = unsafe { ((*sh).prop_size as usize) * std::mem::size_of::<JSProperty>() };
690-
unsafe { std::ptr::write_bytes(new_prop as *mut u8, 0, size_bytes) };
691-
} else if !old_prop.is_null() && unsafe { (*sh).prop_size } > prev_prop_size {
692-
// Zero only the newly-added slots so we don't overwrite existing properties.
693-
let start_index = prev_prop_size as usize;
694-
let new_slots = (unsafe { (*sh).prop_size } - prev_prop_size) as usize;
695-
if new_slots > 0 {
696-
let start_ptr = unsafe { new_prop.add(start_index) } as *mut u8;
697-
let bytes = new_slots * std::mem::size_of::<JSProperty>();
698-
unsafe { std::ptr::write_bytes(start_ptr, 0, bytes) };
700+
if old_prop.is_null() && unsafe { (*sh).prop_size } > 0 {
701+
let size = unsafe { (*sh).prop_size as usize } * std::mem::size_of::<JSProperty>();
702+
let new_prop = unsafe { (*(*ctx).rt).js_realloc_rt(std::ptr::null_mut(), size) as *mut JSProperty };
703+
if new_prop.is_null() {
704+
return -1;
705+
}
706+
unsafe {
707+
(*p).prop = new_prop;
708+
// initialize slots to JS_UNDEFINED
709+
let n = (*sh).prop_size as isize;
710+
for i in 0..n {
711+
(*new_prop.offset(i)).u.value = JS_UNDEFINED;
712+
}
699713
}
700714
}
701715

@@ -877,6 +891,17 @@ pub unsafe fn JS_NewObject(ctx: *mut JSContext) -> JSValue {
877891
}
878892
(*obj).prop = std::ptr::null_mut();
879893
(*obj).first_weak_ref = std::ptr::null_mut();
894+
(*obj).next_in_shape = std::ptr::null_mut();
895+
896+
// Link object into its shape's object list so that when the shape changes
897+
// (for example when its prop_size grows) we can update all objects using
898+
// the same shape.
899+
if !(*obj).shape.is_null() {
900+
let sh = (*obj).shape;
901+
// prepend to shape's list
902+
(*obj).next_in_shape = (*sh).first_object;
903+
(*sh).first_object = obj;
904+
}
880905
JSValue::new_ptr(JS_TAG_OBJECT, obj as *mut c_void)
881906
}
882907
}
@@ -6967,9 +6992,29 @@ unsafe fn js_free_object(rt: *mut JSRuntime, v: JSValue) {
69676992
(*rt).js_free_rt((*p).prop as *mut c_void);
69686993
(*p).prop = std::ptr::null_mut();
69696994
}
6970-
// Free shape
6995+
// Unlink from shape's object list and free shape only if no objects remain
69716996
if !(*p).shape.is_null() {
6972-
(*rt).js_free_shape((*p).shape);
6997+
let sh = (*p).shape;
6998+
// unlink p from sh->first_object list
6999+
if (*sh).first_object == p {
7000+
(*sh).first_object = (*p).next_in_shape;
7001+
} else {
7002+
let mut prev = (*sh).first_object;
7003+
while !prev.is_null() {
7004+
if (*prev).next_in_shape == p {
7005+
(*prev).next_in_shape = (*p).next_in_shape;
7006+
break;
7007+
}
7008+
prev = (*prev).next_in_shape;
7009+
}
7010+
}
7011+
// clear link for p
7012+
(*p).next_in_shape = std::ptr::null_mut();
7013+
7014+
// if no objects remain using this shape, free it
7015+
if (*sh).first_object.is_null() {
7016+
(*rt).js_free_shape(sh);
7017+
}
69737018
(*p).shape = std::ptr::null_mut();
69747019
}
69757020
// Free object struct

tests/refcount.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,87 @@ fn test_define_property_overwrite_after_resize() {
177177
JS_FreeRuntime(rt);
178178
}
179179
}
180+
181+
#[test]
182+
fn test_shared_shape_multiple_objects() {
183+
unsafe {
184+
let rt = JS_NewRuntime();
185+
assert!(!rt.is_null());
186+
let ctx = JS_NewContext(rt);
187+
assert!(!ctx.is_null());
188+
189+
// create two objects
190+
let obj1 = JS_NewObject(ctx);
191+
let obj2 = JS_NewObject(ctx);
192+
assert_eq!(obj1.get_tag(), JS_TAG_OBJECT);
193+
assert_eq!(obj2.get_tag(), JS_TAG_OBJECT);
194+
195+
// Make obj2 share obj1's shape.
196+
let sh1 = (*obj1.get_ptr().cast::<JSObject>()).shape;
197+
let sh2 = (*obj2.get_ptr().cast::<JSObject>()).shape;
198+
199+
// Unlink obj2 from its original shape list and free that shape
200+
if !sh2.is_null() {
201+
if (*sh2).first_object == obj2.get_ptr().cast::<JSObject>() {
202+
(*sh2).first_object = (*obj2.get_ptr().cast::<JSObject>()).next_in_shape;
203+
} else {
204+
let mut prev = (*sh2).first_object;
205+
while !prev.is_null() {
206+
if (*prev).next_in_shape == obj2.get_ptr().cast::<JSObject>() {
207+
(*prev).next_in_shape = (*obj2.get_ptr().cast::<JSObject>()).next_in_shape;
208+
break;
209+
}
210+
prev = (*prev).next_in_shape;
211+
}
212+
}
213+
(*obj2.get_ptr().cast::<JSObject>()).next_in_shape = std::ptr::null_mut();
214+
(*rt).js_free_shape(sh2);
215+
}
216+
217+
// attach obj2 to sh1 list and set its shape pointer
218+
let obj2_ptr = obj2.get_ptr() as *mut JSObject;
219+
(*obj2_ptr).shape = sh1;
220+
(*obj2_ptr).next_in_shape = (*sh1).first_object;
221+
(*sh1).first_object = obj2_ptr;
222+
223+
// Add many properties to obj1 to cause shape growth. The shape is shared,
224+
// so obj2's prop array should also grow (but remain undefined values).
225+
for i in 0..10 {
226+
let key = format!("x{}", i);
227+
let k = std::ffi::CString::new(key.clone()).unwrap();
228+
let atom = (*rt).js_new_atom_len(k.as_ptr() as *const u8, key.len() as usize);
229+
assert!(atom != 0);
230+
let val = JSValue::new_int32(i);
231+
let r = JS_DefinePropertyValue(ctx, obj1, atom, val, 0);
232+
assert_eq!(r, 1);
233+
}
234+
235+
// obj1 should have its properties set
236+
for i in 0..10 {
237+
let key = format!("x{}", i);
238+
let k = std::ffi::CString::new(key.clone()).unwrap();
239+
let atom = (*rt).js_new_atom_len(k.as_ptr() as *const u8, key.len() as usize);
240+
let got1 = JS_GetProperty(ctx, obj1, atom);
241+
assert_eq!(got1.get_tag(), JS_TAG_INT);
242+
assert_eq!(got1.u.int32, i);
243+
244+
// obj2 should be undefined for those properties
245+
let got2 = JS_GetProperty(ctx, obj2, atom);
246+
assert_eq!(got2.get_tag(), JS_TAG_UNDEFINED);
247+
}
248+
249+
// Free obj1 — shape should still exist because obj2 uses it
250+
JS_FreeValue(rt, obj1);
251+
// After freeing obj1, obj2 should still be retrievable for a previously added key
252+
let key0 = std::ffi::CString::new("x0").unwrap();
253+
let atom0 = (*rt).js_new_atom_len(key0.as_ptr() as *const u8, 2);
254+
let got2_after = JS_GetProperty(ctx, obj2, atom0);
255+
assert_eq!(got2_after.get_tag(), JS_TAG_UNDEFINED);
256+
257+
// Free obj2 — this should free the shape as well (no objects remain)
258+
JS_FreeValue(rt, obj2);
259+
260+
JS_FreeContext(ctx);
261+
JS_FreeRuntime(rt);
262+
}
263+
}

0 commit comments

Comments
 (0)