Skip to content

Commit

Permalink
Fix issues between reference counting and the custom JSON.parse imp…
Browse files Browse the repository at this point in the history
…lementation (#673)

* Introduce a minimal failing test

This commit introduces a minimal failing test case where SIMD JSON
parsing and reference couting causes a panic.

Analysis:

Even though our fork of rquickjs disallows GC, it doesn't prevent
reference counting. This, combined with the usage of
`JS_DefinePropertyValue` which decrements the reference count of each
String value inside a map, introduced the bug showcased in this commit.

* Fix issues with reference counting and SIMD JSON (parsing)

This commit is follow-up to the previous commit, in which the
relationship between reference counting and String Object value creation
is fixed by:

- Avoiding the usage of `unsafe` APIs
- Ensuring that everytime a string value gets created it gets correctly
  taken into acount.

Using rquickjs' safe APIs ensures that new values get cloned, which
means that `JS_DupValue` is called, which correctly increases the
reference count.

* Gate the misc test with #[cfg(feature = 'json')]

* Clippy fixes
  • Loading branch information
saulecabrera authored Jun 20, 2024
1 parent 78e5e9d commit 21e7c97
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 25 deletions.
34 changes: 9 additions & 25 deletions crates/javy/src/serde/ser.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use crate::quickjs::{
qjs::{JS_DefinePropertyValue, JS_ValueToAtom, JS_PROP_C_W_E},
Array, Ctx, Object, String as JSString, Value,
};
use crate::quickjs::{object::Property, Array, Ctx, Object, String as JSString, Value};
use crate::serde::err::{Error, Result};
use anyhow::anyhow;

Expand Down Expand Up @@ -340,29 +337,16 @@ impl<'a> ser::SerializeMap for &'a mut Serializer<'_> {
{
let mut map_serializer = Serializer::from_context(self.context.clone())?;
value.serialize(&mut map_serializer)?;
let atom = unsafe { JS_ValueToAtom(self.context.as_raw().as_ptr(), self.key.as_raw()) };

if let Some(o) = self.value.as_object() {
// Use `JS_DefinePropertyValue` to keep the semantics of the object
// unchanged.
let result = unsafe {
JS_DefinePropertyValue(
self.context.as_raw().as_ptr(),
o.as_raw(),
atom,
map_serializer.value.as_raw(),
JS_PROP_C_W_E as i32,
)
};

return if result != 0 {
Ok(())
} else {
Err(Error::custom("Error while serializing object"))
};
let prop = Property::from(map_serializer.value.clone())
.writable()
.configurable()
.enumerable();
o.prop::<_, _, _>(self.key.clone(), prop)
.map_err(|e| Error::custom(e.to_string()))
} else {
Err(Error::custom("Expected to be an object"))
}

Err(Error::custom("Expected to be an object"))
}

fn end(self) -> Result<()> {
Expand Down
21 changes: 21 additions & 0 deletions crates/javy/tests/misc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use anyhow::Result;
use javy::{quickjs::context::EvalOptions, Config, Runtime};

#[cfg(feature = "json")]
#[test]
fn string_keys_and_ref_counting() -> Result<()> {
let mut config = Config::default();
config.override_json_parse_and_stringify(true);

let source = include_bytes!("string_keys_and_ref_counting.js");
let rt = Runtime::new(config)?;

rt.context().with(|this| {
let _: () = this
.eval_with_options(*source, EvalOptions::default())
.inspect_err(|e| println!("{e}"))
.expect("source evaluation to succeed");
});

Ok(())
}
8 changes: 8 additions & 0 deletions crates/javy/tests/string_keys_and_ref_counting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const input = JSON.parse('{"elements":[{"id":"zza"},{"id":"zzb"},{"id":"zzc"},{"id":"zzd"},{"id":"zze"}]}');

const acc = {};
input.elements.forEach(e => {
if (!acc[e.id]) {
acc[e.id] = 1;
}
});

0 comments on commit 21e7c97

Please sign in to comment.