Skip to content

fix(interop): two ABI defects, struct field order and the announced API version - #2

Open
Juliusz Kopczewski (julkiewicz) wants to merge 3 commits into
readycodeio:mainfrom
julkiewicz:2026-08-07-interop-abi-ordering
Open

fix(interop): two ABI defects, struct field order and the announced API version#2
Juliusz Kopczewski (julkiewicz) wants to merge 3 commits into
readycodeio:mainfrom
julkiewicz:2026-08-07-interop-abi-ordering

Conversation

@julkiewicz

@julkiewicz Juliusz Kopczewski (julkiewicz) commented Aug 7, 2026

Copy link
Copy Markdown

Two ABI defects in the interop, both of which compiled, loaded, passed the API hash check, and then crashed or
silently mis-dispatched at runtime. Found while checking the open item to repoint cef-version.json at our
published CEF build.

1. Struct field order

The generated structs mirror CEF's C API structs field for field, so their field order is ABI. The generator
emitted C++ declaration order. CEF's own translator emits version order: a member added by a later API version
is appended after every member of earlier versions, wherever its C++ declaration sits. That is what lets a client
built against an older version still find its members at the offsets it expects.

Where the two disagreed, a member landed mid-struct and shifted every member below it onto the wrong function
pointer. cef_browser_host_t was the worst case: release_accelerated_paint_surface sat 30 slots early, putting
send_key_event onward, through get_runtime_style, on the wrong entry. cef_command_line_t,
cef_download_item_t and cef_request_context_t were wrong the same way, and had been before we patched
anything: the trigger is any method carrying an added= annotation.

Fixed by sorting members by the version that added them, stably, so declaration order still decides within a
version.

2. The announced API version

A client announces its API version by calling cef_api_hash(version) before initializing, and the library then
builds its C API structs to match. Announce an explicit numbered version and every method added since is compiled
out of those structs, so a binding that knows about a newer method reads a different method's pointer, or reads
past the end.

The generator picked CEF_API_VERSION_LAST, so the bindings announced 15000 while being generated from the
experimental headers. ReleaseAcceleratedPaintSurface then jumped through whatever that field position holds in
a 15000-layout struct and took the process down with an access violation.

Fixed by announcing experimental, which is where CEF puts API not yet frozen into a numbered version, and so
where our additions live. It costs the compatibility across a range of CEF releases that an explicit version
buys, which is a fair trade while we ship the libcef we bind against.

Verification

Static: extracted the field order of all 130 generated structs and diffed each against the corresponding capi
struct in the CEF distribution we ship, resolving inherited bases and treating cef_base_ref_counted_t as the
opaque _base field the generator emits. Before: 4 mismatched. After: all 130 match.

Runtime: a managed harness now drives a real browser through these bindings against our patched libcef
(readym-cef, harness/glue). A 10 second run gives 368 accelerated paints, every one carrying a lease id,
every lease released through the managed binding, BGRA8888 at 1280x720, no software fallback, clean shutdown.
Reproducing either defect kills that run, so the check has teeth rather than merely passing.

Also here

The vendored CefGlue.Interop.Gen/include/ tree was still the 150.0.11 baseline and did not contain
ReleaseAcceleratedPaintSurface at all, so regeneration would have quietly deleted the hand-added binding.
Overlays the headers from the build we actually ship and regenerates from them.

cef_version deliberately stays an upstream release number: Directory.Packages.props pins
chromiumembeddedframework.runtime at $(CefVersion) and it is referenced unconditionally, so a value with no
published package fails restore on every platform. UPGRADE.md now explains why the two version fields
diverge, including the consequence that the Linux and macOS cef.runtime.* packages cannot be built from our
cef_build_version, which the CDN does not host.

The include tree was still the 150.0.11 baseline, so it did not contain
ReleaseAcceleratedPaintSurface at all. The binding for it was added by hand and
the version constants were set by hand to a build nobody had vendored, which
left regeneration as a trap: it would have quietly deleted the binding.

Overlays the headers from the build we actually ship, cef_binary_150.0.23-
2026-08-04-surface-lease.3556+ge7ab2e1+chromium-150.0.7871.187_windows64_readym,
and regenerates version.g.cs from them, so the constants now name that build
rather than the stock 150.0.17 they claimed.

cef_version stays an upstream release number on purpose. Directory.Packages.props
pins chromiumembeddedframework.runtime at $(CefVersion) and CefGlue.Packages.props
references it unconditionally, so a value with no published package fails restore
everywhere. UPGRADE.md now explains why the two version fields diverge.
The generated structs mirror CEF's C API structs field for field, so their order
is ABI. The generator emitted C++ declaration order; CEF's own translator emits
version order, appending members added by a later API version after every member
of earlier ones. That is what keeps a client built against an older version
finding the members it knows at the offsets it expects.

Where the two disagreed, a member landed mid-struct and silently shifted every
member below it, so calls dispatched through the wrong function pointer.
cef_browser_host_t was the worst: release_accelerated_paint_surface sat 30 slots
early, which put send_key_event onward and everything through get_runtime_style
on the wrong entry. cef_command_line_t, cef_download_item_t and
cef_request_context_t were wrong the same way, and had been before we patched
anything, since the cause is any method carrying an added= annotation.

Sorts by added version, stably, so declaration order still decides within a
version. Verified by extracting the field order of all 130 generated structs and
diffing each against the corresponding capi struct in the CEF distribution we
ship: all 130 now match, resolving inherited bases and treating
cef_base_ref_counted_t as the opaque _base field the generator emits.
…ered one

A client announces its API version by calling cef_api_hash(version) before
initializing, and the library then builds its C API structs to match. This is not
bookkeeping: announce an explicit version and every method added since is compiled
out of those structs, so a binding that knows about a newer method reads a
different method's pointer, or reads past the end of the struct.

The generator picked CEF_API_VERSION_LAST, so the bindings announced 15000 while
being generated from the experimental headers. Calling
ReleaseAcceleratedPaintSurface then jumped through whatever that field position
holds in a 15000-layout struct and took the process down with an access violation.

Our own additions live in the experimental surface, since that is where CEF puts
API not yet frozen into a numbered version, so that is the version to announce.
It costs the forward and backward compatibility an explicit version buys across a
range of CEF releases, which is a fair trade while we ship the libcef we bind
against.

Verified by running a browser through the managed harness: 368 accelerated paints,
every one carrying a lease id, every lease released, clean shutdown. Before this
change the same run died on the first release call.
@julkiewicz Juliusz Kopczewski (julkiewicz) changed the title fix(interop): order struct members by the version that added them fix(interop): two ABI defects, struct field order and the announced API version Aug 7, 2026
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