Skip to content

Comments

Add programmatic CDP message passing API for BrowserView#148

Open
uxfreak wants to merge 5 commits intoblackboardsh:mainfrom
uxfreak:feat/cdp-message-api
Open

Add programmatic CDP message passing API for BrowserView#148
uxfreak wants to merge 5 commits intoblackboardsh:mainfrom
uxfreak:feat/cdp-message-api

Conversation

@uxfreak
Copy link

@uxfreak uxfreak commented Feb 16, 2026

Summary

Adds a TypeScript API on BrowserView for sending Chrome DevTools Protocol commands and receiving events, using CEF's built-in SendDevToolsMessage / AddDevToolsMessageObserver APIs. Includes follow-up fixes for ARM64 stability, sandbox safety, and BrowserView layout management.

This works entirely in-process — no remote debugging port required, no network exposure.

Motivation

Apps embedding webviews often need programmatic access to CDP for tasks like capturing screenshots, evaluating JavaScript, inspecting the DOM, or monitoring network activity. The current approach requires enabling remote-debugging-port via chromium flags, which opens a network port that's unsuitable for production.

CEF has supported in-process CDP message passing since CEF 86+ via CefBrowserHost::SendDevToolsMessage() and AddDevToolsMessageObserver(). This PR exposes those APIs through ElectroBun's FFI layer.

API

const view = mainWindow.webview;

// Attach the CDP observer (auto-attaches on first cdpSend/cdpOn call)
view.cdpAttach();

// Send a CDP command — returns a Promise with the parsed result
const { result } = await view.cdpSend('Runtime.evaluate', {
  expression: 'document.title',
});

// Capture a screenshot
const { data } = await view.cdpSend('Page.captureScreenshot', {
  format: 'png',
});

// Subscribe to CDP events
const unsub = view.cdpOn('Page.loadEventFired', (params) => {
  console.log('Page loaded:', params);
});

// Subscribe to all CDP events
const unsubAll = view.cdpOnAll((method, params) => {
  console.log(method, params);
});

// Clean up
view.cdpDetach();

BrowserView Layout APIs

// Resize and reposition a view
view.setFrame({ x: 0, y: 32, width: 800, height: 568 });

// Show/hide without destroying DOM state
view.setHidden(true);

// Remove from parent window (auto-detaches CDP observer)
view.remove();

Commits

1. feat: add programmatic CDP message passing API

Native layer (macOS):

  • ElectrobunDevToolsObserverCefDevToolsMessageObserver subclass that routes method results and events to Bun via threadsafe callbacks
  • Heap-allocated buffer copies for cross-thread safety (CEF UI thread → Bun event loop)
  • cdpFreeBuffer() for deterministic cleanup from JS after reading
  • 5 new extern C exports: setDevToolsCDPCallbacks, webviewSendDevToolsMessage, webviewAddDevToolsObserver, webviewRemoveDevToolsObserver, cdpFreeBuffer

FFI layer (native.ts):

  • Threadsafe JSCallbacks with toArrayBuffer for safe pointer-to-string conversion
  • Lazy callback registration (deferred to first cdpAttach() call)

TypeScript API (BrowserView.ts):

  • Promise-based request/response matching via message ID
  • Event subscriptions (per-method and global)
  • Multi-webview support — handlers dispatch by webviewId
  • Auto-attach on first use

2. fix: replace JSCallback with poll-based CDP queue to eliminate SIGTRAP crashes

Problem: JSCallback with threadsafe: true uses TCC-generated trampolines that lack ARM64 PAC (Pointer Authentication Code) instructions. When CEF calls back from its UI thread on Apple Silicon, this causes intermittent SIGTRAP crashes.

Solution: Replace callback-based delivery with thread-safe std::deque queues (mutex-protected) polled from JavaScript at 1ms intervals.

  • New C++ queue types: CDPQueuedResult, CDPQueuedEvent
  • 5 new FFI exports: cdpEnableQueue, cdpDequeueResult, cdpDequeueEvent, cdpResultQueueSize, cdpEventQueueSize, cdpFreeBuffer
  • JavaScript poll loop via setInterval(cdpPollTick, 1) drains both queues

3. fix: detach CEF browser on view removal instead of CloseBrowser

Calling CloseBrowser() on view removal caused cleanup order issues. Changed to webviewRemove() with proper observer detachment for safe lifecycle management.

4. fix: pass sandbox flag via setNextWebviewFlags to avoid FFI param loss

Bun FFI has parameter count limits that were silently dropping the sandbox boolean. Fixed by using setNextWebviewFlags() to pass the flag before initWebview().

5. feat: skip response filter + preload for sandboxed CEF views, add BrowserView layout APIs

  • Skip HTML response filter for sandboxed CEF views (prevents SIGTRAP conflicts with CDP)
  • Skip preload script injection for sandboxed views (security: no RPC bridges for untrusted content)
  • New BrowserView APIs: setFrame(), setHidden(), remove()

Files Changed

File What
src/native/shared/devtools_cdp.h Shared callback typedefs (new)
src/native/macos/nativeWrapper.mm Observer class, queue implementation, extern C exports
src/native/win/nativeWrapper.cpp Stub exports
src/native/linux/nativeWrapper.cpp Stub exports
src/bun/proc/native.ts FFI symbols, poll loop, queue drain, cdpNative export
src/bun/core/BrowserView.ts Public API: cdpAttach/Detach/Send/On/OnAll, setFrame, setHidden, remove

Windows/Linux: Stub implementations that allow the FFI layer to load without link errors. The same CEF APIs (SendDevToolsMessage, AddDevToolsMessageObserver) are available on all platforms — the macOS implementation serves as reference.

Testing

Tested in a CEF-based ElectroBun app on macOS arm64 (Apple Silicon M-series). Verified:

  • Runtime.evaluate("document.title") → correct string result
  • DOM.getDocument({depth: 1}) → full DOM tree with node IDs
  • Page.captureScreenshot({format: "png"}) → valid base64 PNG (576KB)
  • Runtime.evaluate("2 + 2"){type: "number", value: 4}
  • Sustained CDP usage over 1000+ calls with no SIGTRAP crashes (poll queue fix verified)
  • Sandboxed views: no preload injection, no response filter, CDP still functional
  • setFrame() / setHidden() / remove() lifecycle with proper CDP observer cleanup

uxfreak and others added 5 commits February 16, 2026 19:55
… API

Add BrowserView.cdpSend() / cdpOn() / cdpOnAll() methods that enable
sending CDP commands and receiving events without a remote debugging port.

Uses CEF's built-in CefBrowserHost::SendDevToolsMessage() and
AddDevToolsMessageObserver() APIs (stable since CEF 86+), which work
entirely in-process — no network port, no security exposure.

This enables embedding apps to programmatically:
- Capture screenshots (Page.captureScreenshot)
- Evaluate JavaScript (Runtime.evaluate)
- Inspect DOM (DOM.getDocument)
- Monitor network (Network.enable)
- Access the full CDP protocol surface

Implementation:
- New CefDevToolsMessageObserver subclass (ElectrobunDevToolsObserver)
- 4 new extern "C" FFI exports (setDevToolsCDPCallbacks,
  webviewSendDevToolsMessage, webviewAddDevToolsObserver,
  webviewRemoveDevToolsObserver)
- Shared callback types header (devtools_cdp.h)
- Promise-based TypeScript API on BrowserView with event subscriptions
- Stub implementations for Windows and Linux (macOS fully implemented)

Usage:
  const view = mainWindow.webview;
  view.cdpAttach();
  const result = await view.cdpSend('Runtime.evaluate', {
    expression: 'document.title'
  });
  view.cdpOn('Page.loadEventFired', (params) => { ... });
…wserView layout APIs

Sandboxed views now bypass the HTML response filter and preload script
injection in CEF. This prevents SIGTRAP crashes when CDP DevTools agent
is attached — the response filter was conflicting with CDP's internal
plumbing on navigation events.

Also adds setFrame(), remove(), and setHidden() public methods to
BrowserView for programmatic layout management (split-pane UIs).
CloseBrowser(false) sends [window performClose:] to the host NSWindow,
which closes the entire window when multiple BrowserViews share it.
Replace with DetachBrowser() — removes from handler tracking list and
releases the reference without triggering the window close lifecycle.
Bun FFI silently drops boolean parameters at position 19+ in function
calls with many arguments. The sandbox flag (the last param of
initWebview) was always arriving as false in the native layer, causing
sandboxed BrowserViews to get response filters that crash with SIGTRAP
when CDP DevTools agent is attached.

Move sandbox into the existing setNextWebviewFlags pre-set mechanism
(which already worked around this for startTransparent/startPassthrough)
so it's reliably passed to all three platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…P crashes

JSCallback with threadsafe:true uses TCC-generated trampolines that lack
ARM64 PAC instructions, causing intermittent SIGTRAP crashes when CEF
calls back from its UI thread. This replaces the callback approach with
thread-safe std::deque queues polled from JS via setInterval(1ms).

C++ side: cdpEnableQueue(), cdpDequeueResult(), cdpDequeueEvent() with
packed binary buffers. JS side: 1ms poll loop draining both queues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant