gpuijs

Internals

Wire protocol

This page documents the messages between the app process and the host. You only need it to work on gpuijs itself, or to debug the bridge. The channels are described in Architecture.

The authoritative definitions are src/protocol.rs (app → host), js/src/wire.js (the binary commit encoder) and the dispatch function in js/src/renderer.js (host → app).

#Framing (app → host)

Everything the app sends goes through the GPUIJS_IN pipe as frames:

┌─────────┬──────────────────┬───────────────────────┐
│ kind u8 │ length u32 (LE)  │ body (length bytes)   │
└─────────┴──────────────────┴───────────────────────┘
kind 0: body is a JSON message
kind 1: body is a binary commit

The host stops reading if a frame claims more than 1 GiB, since the stream must be out of step by then.

#JSON messages (app → host)

Each is an object tagged by t.

tFieldsMeaning
windowoptionsnew Window(options). Always the first message. The window opens with these.
csstextReplaces the app's stylesheet (the compiled app.css)
stylessheets: string[]Replaces the runtime <style> sheets, in document order
syncThe app is blocked until the host writes the next layout's rects to GPUIJS_SYNC
settledJavaScript has finished handling the input events it was sent
benchreq, rect?Answers a benchmark query or settle request
menuitemsSets the application menu
dockmenuitemsSets the Dock menu
popupreq, itemsShows a context menu. The host answers menu-closed with req.
winop, argA Window method: setTitle, minimize, maximize, unmaximize, setFullScreen, close, focus, setSize
appopAn app method: quit, hide, show, focus
dialogreq, kind, optionsopen, save or message. The host answers dialog with req.

A menu item is { id, label, type, role, accelerator, checked, enabled, submenu }, with id being the item's commandId.

#Binary commits (app → host)

One React commit is one binary frame, so it applies atomically. A 10,000-row mount is megabytes of ops, and JSON took longer to write and parse than the host took to apply them.

Everything is little-endian. Ids are u32. A string is a u32 byte length followed by UTF-8.

commit  := count:u32  op*
op      := code:u8  fields
CodeOpFieldsMeaning
1elid:u32 tag:str propsCreate an element
2txtid:u32 text:strCreate a text node
3setid:u32 propsReplace an element's props
4settextid:u32 text:strReplace a text node's text
5appendparent:u32 child:u32Append a child
6insertparent:u32 child:u32 before:u32Insert a child before a sibling
7removeparent:u32 child:u32Remove and free a subtree
8detachparent:u32 child:u32Unlink without freeing (a library may attach the node again)
9focushas:u8 id:u32?JavaScript moved focus to a node, or cleared it
10hoverhas:u8 id:u32?The SVG shape under the pointer, found by JavaScript hit-testing, or none

#Props

Props start with a u16 mask of the fields present, and then those fields in bit order:

BitFieldEncodingMeaning
0classstrClass list
1idstrElement id
2stylestrInline style as CSS declaration text
3eventsu8 count, then strsEvents the host should report: click, mousedown, mouseup, mousemove, mouseenter, mouseleave, keydown, contextmenu, change
4attrsu16 count, then key/value str pairsAttributes for selectors and the host (role, type, href, data-*, aria-*, SVG attributes, ...)
5valuestrA field's value
6placeholderstrA field's placeholder
7disablednone (the bit alone)Disabled
8srcstrAn image's resolved local path
9sequ32An input's last change sequence the app has seen
10measurenone (the bit alone)Report this node's rect after each layout

#Host → app messages

The host writes newline-delimited JSON to GPUIJS_SOCKET.

#metrics

{"t": "metrics", "regular": [0.27, 0.31, ...], "bold": [0.28, ...]}

Sent first. Advance widths of printable ASCII (space through ~) in the system UI font, in ems, regular and bold. Rendering waits for this, and measureText uses it.

#event

{"t": "event", "id": 42, "type": "click", "clientX": 120, "clientY": 88, "target": 57}

All other messages are events with a node id (0 is <html>) and a type:

typeExtra fieldsMeaning
click, mousedown, mouseup, mousemove, mouseenter, mouseleave, contextmenuclientX, clientY, target?Pointer events on a node that listens. target is the innermost box under the pointer.
documentevents, clientX, clientY, target?Pointer events no node reported, for document listeners
keydownkey, metaKey, ctrlKey, shiftKey, altKeyA key press. On <html> for the window, or on a text field for its own keys.
changevalue, seq?A text field edit (with its sequence number) or a <select> pick
focus, blurA native field gained or lost focus
resizerectA measured node's new rect: { x, y, width, height, padding, border }
visibilitychangehiddenThe window was hidden or shown
appearancedarkThe system appearance changed
windowname, focused, maximized, fullScreen, boundsA window event (ready-to-show, focus, blur, resize, close) with the window's state
appnameAn app event (activate)
menuitemA menu item was chosen (its commandId)
menu-closedreqA context menu closed
dialogreq, and the resultA dialog closed
benchQuery, benchSettlereq, selector?, index?Benchmark driver requests

#Sync answers

When the app sends sync, the host applies everything before it, lays out, and writes one line to GPUIJS_SYNC:

{"rects": {"42": {"x": 0, "y": 64, "width": 320, "height": 48, "padding": [0, 0, 0, 0], "border": [0, 0, 1, 0]}}}

It holds the rect of every measured node that changed. The app applies them as resize events before returning from the geometry read that forced the layout.

#Input sequence numbers

Text fields live in the host. Each edit increments the field's seq and reports change with it. When the app sends props for an input, they carry the last seq it has seen, and the host applies a value only if it isn't older than the field's last edit. A controlled input therefore never rolls back keystrokes typed while React was busy.