Skip to content

fix: copy JSON string before dispatch_async in menu functions#192

Open
stefandevo wants to merge 1 commit intoblackboardsh:mainfrom
stefandevo:fix/setApplicationMenu-dispatch-async-buffer
Open

fix: copy JSON string before dispatch_async in menu functions#192
stefandevo wants to merge 1 commit intoblackboardsh:mainfrom
stefandevo:fix/setApplicationMenu-dispatch-async-buffer

Conversation

@stefandevo
Copy link

@stefandevo stefandevo commented Feb 23, 2026

Summary

  • Copy const char *jsonString into an NSString (ARC-managed) before dispatch_async in all three affected functions: setApplicationMenu, showContextMenu, and setTrayMenuFromJSON
  • The dispatch block now captures the NSString copy, which is retained by ARC regardless of Bun's GC timing

Problem

The native Obj-C functions receive a const char * from Bun's FFI and pass it into a dispatch_async(dispatch_get_main_queue(), ...) block. The block captures the raw pointer by value. By the time the main thread dequeues and executes the block, Bun's garbage collector has freed or overwritten the memory at that address, resulting in:

Failed to parse JSON: Error Domain=NSCocoaErrorDomain Code=3840
  "Unable to parse empty data."

The menu bar shows only the app name with no subitems or additional menus.

Root cause

toCString() in the JS layer creates a Buffer.from(string) and returns ptr(buf). The Buffer is a local variable that becomes GC-eligible immediately after the FFI call returns. For synchronous native calls this is fine (the native code reads the string before returning), but dispatch_async defers the read to a later main-thread tick — by which time the buffer is gone.

A JS-side workaround (pinning the buffer to globalThis) was attempted in dist-macos-arm64 but:

  1. The dist/ copy (which is what package.json exports actually resolve to) was never patched
  2. Even with pinning, Buffer.from() may use Bun's pooled slab allocator, and subsequent buffer allocations can overwrite the slab region

Fix

The proper fix is on the native side — one line per function:

NSString *jsonCopy = [NSString stringWithUTF8String:jsonString];

Called before dispatch_async, so the block captures an ARC-retained NSString instead of a raw const char *. The NSData is then created from the copy inside the block:

NSData *jsonData = [jsonCopy dataUsingEncoding:NSUTF8StringEncoding];

Affected functions

Function File Line
setApplicationMenu nativeWrapper.mm ~6480
showContextMenu nativeWrapper.mm ~6499
setTrayMenuFromJSON nativeWrapper.mm ~6449

Test plan

  • Verified fix resolves the issue in a real Electrobun app (DevFlow) using setApplicationMenu with top-level await in the entry point
  • Build native dylib and verify menus appear correctly
  • Verify context menus still work
  • Verify tray menus still work

Fixes #160, #136, #191

🤖 Generated with Claude Code

The native setApplicationMenu, showContextMenu, and setTrayMenuFromJSON
functions use dispatch_async to schedule work on the main thread, but
capture the raw `const char *` pointer by value. By the time the main
thread dequeues the block, Bun's GC has freed or overwritten the memory
backing that pointer, resulting in:

  Failed to parse JSON: Unable to parse empty data.

Fix: copy the C string into an NSString (owned by ObjC ARC) before the
dispatch_async block, so the block captures an ARC-managed copy that
remains valid regardless of Bun's GC timing.

Fixes blackboardsh#160, blackboardsh#136, blackboardsh#191
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setApplicationMenu: dangling pointer in dispatch_async causes silent JSON parse failure (menu never created)

1 participant