Skip to content

Commit ef60371

Browse files
justjakeclaude
andauthored
Improve QTS_Dump: handle inherited props, better fallback (#241) (#250)
- Fix inherited property handling: Error.name was not included because it's inherited from Error.prototype. Changed qts_should_copy_special_prop to use JS_GetProperty (checks prototype chain) instead of only JS_GetOwnProperty. - Improved fallback when JSON serialization fails (e.g., due to memory limits): - bellard/quickjs: Uses JS_PrintValue with show_hidden=true for rich debug output - quickjs-ng: Falls back to toString() since JS_PrintValue is not available - Format: "{debug output}\n---\nnot JSON serializable: {error message}" - Added tests for: - Error objects include name, message, and stack in dump - Objects with non-enumerable special properties are dumped correctly - Memory-constrained scenarios return informative fallback strings - Added C code style documentation to CLAUDE.md Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent df4efb9 commit ef60371

3 files changed

Lines changed: 300 additions & 38 deletions

File tree

CLAUDE.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,51 @@ If you see assertions like:
141141
- `Assertion failed: p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT` - memory corruption
142142

143143
These usually indicate memory management bugs: double-frees, use-after-free, or missing frees.
144+
145+
## C Code Style in interface.c
146+
147+
### Naming Conventions
148+
149+
- `QTS_` prefix for exported FFI functions (defined in interface.h, called from JavaScript)
150+
- `qts_` prefix for internal C helper functions
151+
- `static` keyword for file-local functions and variables
152+
- Use `bool` from `<stdbool.h>` for boolean values (already included)
153+
154+
### Code Organization
155+
156+
- Section comments with `// ----` separators for logical grouping
157+
- Forward declarations at the top of sections when needed
158+
- Constants defined near related functions
159+
160+
### Error Handling Patterns
161+
162+
- Check `JS_IsException(value)` for error conditions
163+
- Use `JS_GetException(ctx)` to retrieve the exception value
164+
- Clean up resources in reverse order of allocation
165+
- Return `JS_EXCEPTION` or `NULL` to signal errors to callers
166+
167+
### Example
168+
169+
```c
170+
// ----------------------------------------------------------------------------
171+
// Section Name
172+
173+
// Forward declaration
174+
static JSValue qts_helper_function(JSContext *ctx, JSValueConst obj);
175+
176+
// Internal helper - not exported
177+
static bool qts_check_something(JSContext *ctx, JSValueConst obj) {
178+
if (!JS_IsObject(obj)) return false;
179+
// ... implementation
180+
return true;
181+
}
182+
183+
// Exported FFI function
184+
JSValue *QTS_DoSomething(JSContext *ctx, JSValueConst *obj) {
185+
JSValue result = qts_helper_function(ctx, *obj);
186+
if (JS_IsException(result)) {
187+
return jsvalue_to_heap(JS_EXCEPTION);
188+
}
189+
return jsvalue_to_heap(result);
190+
}
191+
```

c/interface.c

Lines changed: 195 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,188 @@ void qts_dump(JSContext *ctx, JSValueConst value) {
158158
putchar('\n');
159159
}
160160

161-
void copy_prop_if_needed(JSContext *ctx, JSValueConst dest, JSValueConst src, const char *prop_name) {
161+
// ----------------------------------------------------------------------------
162+
// QTS_Dump helpers
163+
164+
// Forward declaration
165+
JSBorrowedChar *QTS_GetString(JSContext *ctx, JSValueConst *value);
166+
167+
// Special non-enumerable properties to include when dumping objects (e.g., Error properties)
168+
static const char *QTS_DUMP_SPECIAL_PROPS[] = {
169+
"name", "message", "stack", "fileName", "lineNumber"
170+
};
171+
#define QTS_DUMP_SPECIAL_PROPS_COUNT (sizeof(QTS_DUMP_SPECIAL_PROPS) / sizeof(QTS_DUMP_SPECIAL_PROPS[0]))
172+
173+
// Returns true if prop should be added to the clone:
174+
// - Property exists (own or inherited) and has a non-undefined value
175+
// - Property is NOT already an own enumerable property (which JSON.stringify would include)
176+
static bool qts_should_copy_special_prop(JSContext *ctx, JSValueConst obj, const char *prop_name) {
162177
JSAtom prop_atom = JS_NewAtom(ctx, prop_name);
163-
JSValue dest_prop = JS_GetProperty(ctx, dest, prop_atom);
164-
if (JS_IsUndefined(dest_prop)) {
165-
JSValue src_prop = JS_GetProperty(ctx, src, prop_atom);
166-
if (!JS_IsUndefined(src_prop) && !JS_IsException(src_prop)) {
167-
JS_SetProperty(ctx, dest, prop_atom, src_prop);
178+
179+
// First check if property exists at all (including inherited)
180+
JSValue val = JS_GetProperty(ctx, obj, prop_atom);
181+
if (JS_IsException(val) || JS_IsUndefined(val)) {
182+
JS_FreeAtom(ctx, prop_atom);
183+
JS_FreeValue(ctx, val);
184+
return false; // Property doesn't exist or is undefined
185+
}
186+
JS_FreeValue(ctx, val);
187+
188+
// Property exists - check if it's an own enumerable property
189+
JSPropertyDescriptor desc;
190+
int ret = JS_GetOwnProperty(ctx, &desc, obj, prop_atom);
191+
JS_FreeAtom(ctx, prop_atom);
192+
193+
if (ret != 1) {
194+
// Not an own property - but it exists (inherited), so we should copy it
195+
return true;
196+
}
197+
198+
// It's an own property - check if enumerable
199+
bool is_enumerable = (desc.flags & JS_PROP_ENUMERABLE);
200+
201+
// Free descriptor values
202+
JS_FreeValue(ctx, desc.value);
203+
if (desc.flags & JS_PROP_GETSET) {
204+
JS_FreeValue(ctx, desc.getter);
205+
JS_FreeValue(ctx, desc.setter);
206+
}
207+
208+
// Copy if NOT enumerable (enumerable props are already included via JSON.stringify)
209+
return !is_enumerable;
210+
}
211+
212+
// Creates clone with special props made enumerable, or JS_UNDEFINED if not needed
213+
static JSValue qts_dump_create_clone(JSContext *ctx, JSValueConst obj) {
214+
if (!JS_IsObject(obj)) return JS_UNDEFINED;
215+
216+
// Check if any special props need copying
217+
bool needs_clone = false;
218+
for (size_t i = 0; i < QTS_DUMP_SPECIAL_PROPS_COUNT; i++) {
219+
if (qts_should_copy_special_prop(ctx, obj, QTS_DUMP_SPECIAL_PROPS[i])) {
220+
needs_clone = true;
221+
break;
222+
}
223+
}
224+
if (!needs_clone) return JS_UNDEFINED;
225+
226+
// Create clone with all enumerable props
227+
JSValue clone = JS_NewObject(ctx);
228+
if (JS_IsException(clone)) return clone;
229+
230+
JSPropertyEnum *tab;
231+
uint32_t len;
232+
if (JS_GetOwnPropertyNames(ctx, &tab, &len, obj, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) {
233+
JS_FreeValue(ctx, clone);
234+
return JS_EXCEPTION;
235+
}
236+
237+
for (uint32_t i = 0; i < len; i++) {
238+
JSValue val = JS_GetProperty(ctx, obj, tab[i].atom);
239+
if (!JS_IsException(val)) {
240+
JS_DefinePropertyValue(ctx, clone, tab[i].atom, val, JS_PROP_C_W_E);
241+
}
242+
}
243+
JS_FreePropertyEnum(ctx, tab, len);
244+
245+
// Add special props (inherited or non-enumerable) as enumerable
246+
for (size_t i = 0; i < QTS_DUMP_SPECIAL_PROPS_COUNT; i++) {
247+
if (qts_should_copy_special_prop(ctx, obj, QTS_DUMP_SPECIAL_PROPS[i])) {
248+
JSAtom atom = JS_NewAtom(ctx, QTS_DUMP_SPECIAL_PROPS[i]);
249+
JSValue val = JS_GetProperty(ctx, obj, atom);
250+
if (!JS_IsException(val) && !JS_IsUndefined(val)) {
251+
JS_DefinePropertyValue(ctx, clone, atom, val, JS_PROP_C_W_E);
252+
} else {
253+
JS_FreeValue(ctx, val);
254+
}
255+
JS_FreeAtom(ctx, atom);
168256
}
257+
}
258+
259+
return clone;
260+
}
261+
262+
#define QTS_DUMP_FALLBACK_SIZE 2048
263+
264+
#ifndef QTS_USE_QUICKJS_NG
265+
// Simple fixed buffer for JS_PrintValue output (bellard/quickjs only).
266+
typedef struct qts_debugbuf {
267+
char *buf;
268+
size_t size;
269+
size_t pos;
270+
} qts_debugbuf;
271+
272+
static void qts_debugbuf_write(void *opaque, const char *data, size_t len) {
273+
qts_debugbuf *s = opaque;
274+
size_t remaining = s->size - s->pos - 1; // -1 for null terminator
275+
if (len > remaining) len = remaining;
276+
if (len > 0) {
277+
memcpy(s->buf + s->pos, data, len);
278+
s->pos += len;
279+
s->buf[s->pos] = '\0';
280+
}
281+
}
282+
#endif
283+
284+
// Returns a fallback string when JSON serialization fails
285+
static JSBorrowedChar *qts_dump_get_fallback(JSContext *ctx, JSValueConst obj, JSValue exception) {
286+
char buf[QTS_DUMP_FALLBACK_SIZE];
287+
size_t pos = 0;
288+
buf[0] = '\0';
289+
290+
#ifndef QTS_USE_QUICKJS_NG
291+
// bellard/quickjs: Use JS_PrintValue with show_hidden for rich debug output
292+
qts_debugbuf dbuf = { buf, sizeof(buf), 0 };
293+
294+
JSPrintValueOptions options;
295+
JS_PrintValueSetDefaultOptions(&options);
296+
options.show_hidden = true;
297+
options.max_depth = 3;
298+
options.max_string_length = 200;
299+
options.max_item_count = 20;
300+
301+
JS_PrintValue(ctx, qts_debugbuf_write, &dbuf, obj, &options);
302+
pos = dbuf.pos;
303+
#else
304+
// quickjs-ng: JS_PrintValue not available, use toString() instead
305+
JSValue to_string_val = JS_ToString(ctx, obj);
306+
if (JS_IsException(to_string_val)) {
307+
JS_FreeValue(ctx, JS_GetException(ctx));
308+
pos = snprintf(buf, sizeof(buf), "(toString failed)");
169309
} else {
170-
JS_FreeValue(ctx, dest_prop);
310+
const char *str = JS_ToCString(ctx, to_string_val);
311+
if (str) {
312+
size_t len = strlen(str);
313+
if (len > sizeof(buf) - 100) len = sizeof(buf) - 100; // Reserve space for error info
314+
memcpy(buf, str, len);
315+
buf[len] = '\0';
316+
pos = len;
317+
JS_FreeCString(ctx, str);
318+
}
319+
JS_FreeValue(ctx, to_string_val);
171320
}
172-
JS_FreeAtom(ctx, prop_atom);
321+
#endif
322+
323+
// Add separator and error info
324+
const char *err_msg = NULL;
325+
if (!JS_IsUndefined(exception) && !JS_IsNull(exception)) {
326+
JSValue msg = JS_GetPropertyStr(ctx, exception, "message");
327+
if (!JS_IsException(msg) && JS_IsString(msg)) {
328+
err_msg = JS_ToCString(ctx, msg);
329+
}
330+
JS_FreeValue(ctx, msg);
331+
}
332+
333+
size_t remaining = sizeof(buf) - pos - 1;
334+
snprintf(buf + pos, remaining, "\n---\nnot JSON serializable: %s",
335+
err_msg ? err_msg : "(unknown error)");
336+
337+
if (err_msg) JS_FreeCString(ctx, err_msg);
338+
339+
JSValue result_str = JS_NewString(ctx, buf);
340+
JSBorrowedChar *result = QTS_GetString(ctx, &result_str);
341+
JS_FreeValue(ctx, result_str);
342+
return result;
173343
}
174344

175345
JSValue *jsvalue_to_heap(JSValueConst value) {
@@ -815,40 +985,27 @@ JSValue *QTS_ResolveException(JSContext *ctx, JSValue *maybe_exception) {
815985
}
816986

817987
MaybeAsync(JSBorrowedChar *) QTS_Dump(JSContext *ctx, JSValueConst *obj) {
818-
JSValue obj_json_value = JS_JSONStringify(ctx, *obj, JS_UNDEFINED, JS_UNDEFINED);
819-
if (!JS_IsException(obj_json_value)) {
820-
const char *obj_json_chars = JS_ToCString(ctx, obj_json_value);
821-
JS_FreeValue(ctx, obj_json_value);
822-
if (obj_json_chars != NULL) {
823-
JSValue enumerable_props = JS_ParseJSON(ctx, obj_json_chars, strlen(obj_json_chars), "<dump>");
824-
JS_FreeCString(ctx, obj_json_chars);
825-
if (!JS_IsException(enumerable_props)) {
826-
// Copy common non-enumerable props for different object types.
827-
// Errors:
828-
copy_prop_if_needed(ctx, enumerable_props, *obj, "name");
829-
copy_prop_if_needed(ctx, enumerable_props, *obj, "message");
830-
copy_prop_if_needed(ctx, enumerable_props, *obj, "stack");
831-
copy_prop_if_needed(ctx, enumerable_props, *obj, "fileName");
832-
copy_prop_if_needed(ctx, enumerable_props, *obj, "lineNumber");
833-
834-
// Serialize again.
835-
JSValue enumerable_json = JS_JSONStringify(ctx, enumerable_props, JS_UNDEFINED, JS_UNDEFINED);
836-
JS_FreeValue(ctx, enumerable_props);
837-
838-
JSBorrowedChar *result = QTS_GetString(ctx, &enumerable_json);
839-
JS_FreeValue(ctx, enumerable_json);
840-
return result;
841-
}
842-
}
988+
// Try to create clone with special props; returns JS_UNDEFINED if not needed
989+
JSValue clone = qts_dump_create_clone(ctx, *obj);
990+
JSValue to_serialize = JS_IsUndefined(clone) ? *obj : clone;
991+
992+
// Serialize exactly once
993+
JSValue json = JS_JSONStringify(ctx, to_serialize, JS_UNDEFINED, JS_UNDEFINED);
994+
995+
if (!JS_IsUndefined(clone)) {
996+
JS_FreeValue(ctx, clone);
843997
}
844998

845-
IF_DEBUG {
846-
qts_log("Error dumping JSON:");
847-
js_std_dump_error(ctx);
999+
if (JS_IsException(json)) {
1000+
JSValue exception = JS_GetException(ctx);
1001+
JSBorrowedChar *result = qts_dump_get_fallback(ctx, *obj, exception);
1002+
JS_FreeValue(ctx, exception);
1003+
return result;
8481004
}
8491005

850-
// Fallback: convert to string
851-
return QTS_GetString(ctx, obj);
1006+
JSBorrowedChar *result = QTS_GetString(ctx, &json);
1007+
JS_FreeValue(ctx, json);
1008+
return result;
8521009
}
8531010

8541011
JSValue qts_resolve_func_data(

packages/quickjs-emscripten/src/quickjs.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,63 @@ export default "the default";
798798
dumpTestExample(null)
799799
dumpTestExample({ cow: true })
800800
dumpTestExample([1, 2, 3])
801+
802+
it("includes Error name, message, and stack", () => {
803+
const handle = vm.unwrapResult(vm.evalCode(`new Error("test message")`))
804+
const dumped = vm.dump(handle)
805+
handle.dispose()
806+
807+
assert.strictEqual(dumped.name, "Error")
808+
assert.strictEqual(dumped.message, "test message")
809+
assert(typeof dumped.stack === "string", "stack should be a string")
810+
// QuickJS stack format is " at <function> (file:line:col)\n" - doesn't include message
811+
assert(dumped.stack.includes("at"), "stack should include 'at'")
812+
})
813+
814+
it("includes non-enumerable special properties", () => {
815+
const handle = vm.unwrapResult(
816+
vm.evalCode(`
817+
const obj = { foo: "bar" };
818+
Object.defineProperty(obj, "name", { value: "CustomName", enumerable: false });
819+
Object.defineProperty(obj, "message", { value: "CustomMessage", enumerable: false });
820+
obj;
821+
`),
822+
)
823+
const dumped = vm.dump(handle)
824+
handle.dispose()
825+
826+
assert.strictEqual(dumped.foo, "bar")
827+
assert.strictEqual(dumped.name, "CustomName")
828+
assert.strictEqual(dumped.message, "CustomMessage")
829+
})
830+
831+
it("returns informative fallback when serialization fails due to memory limit", () => {
832+
// Use a memory limit that allows object creation but fails during JSON serialization
833+
vm.runtime.setMemoryLimit(1024 * 200) // 200KB
834+
835+
// Create an object that's too large to serialize
836+
const result = vm.evalCode(`
837+
const big = {};
838+
for (let i = 0; i < 5000; i++) big['key' + i] = 'value' + i;
839+
big;
840+
`)
841+
842+
if (result.error) {
843+
// OOM during eval is acceptable - remove limit and clean up
844+
vm.runtime.setMemoryLimit(-1)
845+
result.error.dispose()
846+
return
847+
}
848+
849+
const dumped = vm.dump(result.value)
850+
result.value.dispose()
851+
vm.runtime.setMemoryLimit(-1) // Remove limit for cleanup
852+
853+
// Format: JS_PrintValue output + "\n---\nnot JSON serializable: ${error}"
854+
assert(typeof dumped === "string", "fallback should be a string")
855+
assert(dumped.includes("---"), "should include separator")
856+
assert(dumped.includes("not JSON serializable"), "should include error context")
857+
})
801858
})
802859

803860
describe(".typeof", () => {

0 commit comments

Comments
 (0)