Case 126: SYCL device Impl Pointer โ shared_ptr โ Raw Pointer¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, API break |
Detected ChangeKinds |
struct_size_changed |
| Source files | examples/case126_sycl_device_impl_ptr/ |
Category: Type Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
sycl::device stores its implementation as a std::shared_ptr<device_impl>
in v1 โ two pointers wide (16 bytes) on a 64-bit target. v2 replaces it with
a raw device_impl * (one word, 8 bytes), so sizeof(sycl::device) shrinks
from 16 to 8. Any consumer that holds a device by value โ on the stack,
embedded in another struct, or inside a container โ reserved storage and
computed offsets using the old 16-byte size; those computations are now
wrong without a recompile. The method mangled names are unchanged, so a pure
exported-symbol diff would miss this entirely; it is a pure layout break.
This case is distilled from a real upstream ABI break: Intel DPC++/oneAPI
SYCL's intel/llvm#20821 (labeled
abi-break), which produced the symptom in
intel/llvm#20915 and was
stabilized by intel/llvm#21028.
The code here is an original, self-contained miniature โ it pulls in no SYCL
headers.
Old/new diff¶
| v1.h | v2.h |
|---|---|
std::shared_ptr<detail::device_impl> impl; |
detail::device_impl *impl; |
sizeof(sycl::device) == 16 |
sizeof(sycl::device) == 8 |
abicheck command¶
g++ -shared -fPIC -g -O0 -fvisibility=hidden -fvisibility-inlines-hidden v1.cpp -o libdev_v1.so
g++ -shared -fPIC -g -O0 -fvisibility=hidden -fvisibility-inlines-hidden v2.cpp -o libdev_v2.so
abicheck compare libdev_v1.so libdev_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: sycl::device (128 -> 64 bits)
> Old code allocates or copies the type with the old size;
heap/stack corruption, out-of-bounds access.
- type_field_type_changed: Field type changed: sycl::device::impl
(shared_ptr<sycl::detail::device_impl> -> device_impl *)
> Field has different size or representation; old code misinterprets
the data.
Minimum evidence¶
min_evidence: L1 โ DWARF's record-layout info (DW_TAG_structure_type
size + member types) is enough to detect both the 16โ8 byte shrink and the
field-type swap directly from the compiled .so's debug info; no public
headers are required for this case.
Why abicheck catches it¶
DWARF records sycl::device's total byte size and its impl member's type
for both binaries; abicheck's DWARF-layout diff compares them directly. A
separate, coarser DWARF-only struct-size check (struct_size_changed) also
fires internally on the same fact but is filtered as redundant once the
richer type_size_changed/type_field_type_changed pair โ which additionally
identifies which field changed and how โ is already reported for the same
symbol. (With a header AST layered on top via --header, the two paths can
diverge and both survive, since the AST and DWARF layers sometimes disagree
on the fully-qualified symbol name used for dedup.)
Runtime failure demonstration¶
Severity: real ABI break, but this specific repro shows no crash
# Build old library + app (app embeds `sycl::device dev;` by value, using v1's 16-byte layout)
g++ -shared -fPIC -g -O0 -fvisibility=hidden -fvisibility-inlines-hidden v1.cpp -o libdev.so
g++ -std=c++17 -g app.cpp -I. -L. -ldev -Wl,-rpath,. -o app
./app
# -> app sees sizeof(sycl::device) = 16 (v1 layout = 16)
# -> device id via library = -1
# -> before = BEFORE!
# -> after = AFTER!!
# Swap in new library (no recompile):
g++ -shared -fPIC -g -O0 -fvisibility=hidden -fvisibility-inlines-hidden v2.cpp -o libdev.so
./app
# -> identical output -- no crash, no corrupted canary
Why no visible corruption here: the size shrank (16 โ 8). The library's
device() constructor now writes fewer bytes than the app reserved for its
16-byte dev member, so nothing gets overwritten in this minimal repro โ
the opposite of case07, where the struct grew and wrote past the app's
smaller allocation. The break is still real: any code that computes offsets
from sizeof(sycl::device) (arrays of devices, structs with fields after a
by-value device, or a second translation unit built against the new
8-byte header) disagrees with this app's 16-byte assumption. That mismatch โ
not a single-process crash โ is exactly what produced the upstream symbol
churn in intel/llvm#20915, and exactly why a static layout diff (not a
runtime smoke test) is the reliable way to catch this class of break.
Safe redesign¶
Keep the public class size stable across releases with the PIMPL idiom: store a single opaque pointer whose pointee layout (and ownership model) can change freely, while the public class's size never does. If the ownership model must change from shared to raw, bump the library SONAME so consumers relink and recompute layout.
Real-world example: intel/llvm#20821
is exactly this change, upstream โ replacing shared_ptr<device_impl> with a
raw pointer inside sycl::device for the DPC++ SYCL runtime.
Cross-tool comparison¶
abidiff also operates from DWARF and would catch the size/field-type
change the same way abicheck's L1 path does โ this repro doesn't need
headers, so both a symbol-and-DWARF tool and abicheck agree here. Where they
diverge is header-only changes (see case124/case125): those have no DWARF
trace at all, which abidiff cannot see regardless of flags.
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.