Skip to content

gen: default GIAC_TYPE_ON_8BITS=1 to fix cross-GCC ABI mismatch#1

Merged
s-celles merged 1 commit into
devfrom
feat/giac-type-on-8bits
May 13, 2026
Merged

gen: default GIAC_TYPE_ON_8BITS=1 to fix cross-GCC ABI mismatch#1
s-celles merged 1 commit into
devfrom
feat/giac-type-on-8bits

Conversation

@s-celles
Copy link
Copy Markdown
Owner

Summary

Defaults the `type` field of `class gen` to a plain 8-bit `unsigned char` rather than the historical `unsigned char type:5; unsigned char type_unused:3;` bitfield, by adding a one-block `#ifndef GIAC_TYPE_ON_8BITS / #define GIAC_TYPE_ON_8BITS 1 / #endif` to `src/dispatch.h`.

Why

The bitfield layout is access-fused differently by different GCC versions. GCC's optimizer combines adjacent bitfield writes (`g.type = ...; g.subtype = ...;`) into a single wider store using version-dependent bit placements. GCC 8 and GCC 10 produce instruction sequences that disagree on which bits of the resulting word hold `type`.

This is a real, user-visible bug in the libgiac_julia / Giac.jl stack on Windows:

  • `GIAC_jll` is built with BinaryBuilder GCC 8 (per its Yggdrasil recipe's `preferred_gcc_version=v"8"`)
  • `libgiac_julia_jll` is built with BinaryBuilder GCC 10
  • When libgiac writes a gen tagged `_REAL` (3) and the wrapper reads it through the JLL pair, the wrapper sees `DOUBLE` (1)

A multi-platform C++ probe (details) compiled the relevant snippets on Linux GCC 12, macOS clang, and Windows MSYS2 GCC 15.2 in both modes:

Platform Default (bitfield) `-DGIAC_TYPE_ON_8BITS`
Linux (same-compiler) `_REAL` ✓ `_REAL` ✓
macOS (same-compiler) `_REAL` ✓ `_REAL` ✓
Windows (same-compiler, static link) `_REAL` ✓ `_REAL` ✓
Linux (consumer GCC 12 vs GIAC_jll GCC 8 dyn link) `_REAL` ✓ SEGFAULT (proves the macro IS load-bearing for ABI)
Production Julia stack (libgiac_julia_jll GCC 10 + GIAC_jll GCC 8) (reported bug per Giac.jl#22) (this PR is the fix)

Same-compiler builds work correctly in both modes. The bug only appears with cross-compiler dynamic loading — exactly the production scenario.

With `GIAC_TYPE_ON_8BITS` the `type` field is a plain byte at offset 0; both GCC 8 and GCC 10 emit a trivial `mov BYTE PTR [g], type`. No fusion, no version-dependent packing. The ABI becomes compiler-invariant.

Cost

Per Bernard Parisse's recommendation (which is what motivated this PR):

"Changer la taille du champ type d'un gen fait perdre 3 bits de mantisse pour les flottants codés dans des gen, on passe de 48 à 45 ... Cela ne change pas la taille d'un gen qui occupe toujours 64 bits ... Le champ type de taille 8 bits a été longtemps le champ par défaut pour gen.h, il devrait donc être assez sécurisé, juste un peu moins précis, mais si c'est plus safe pour les différents compilateurs, alors c'est surement la voie à suivre!"

  • Loses 3 bits of mantissa on inline doubles encoded in a gen (48 → 45)
  • `sizeof(gen)` unchanged at 64 bits
  • Historical default before the bitfield was introduced
  • Documented `#ifndef` guard lets builds override via pre-existing `-DGIAC_TYPE_ON_8BITS` definition; remove the block to opt back into the bitfield layout

Effects on the giac codebase

  • `src/gen.h` line 614 `#ifdef GIAC_TYPE_ON_8BITS` block: takes the `unsigned char type;` branch (the desired behavior). No code change needed.
  • `src/giac.i` and `src/giacjs.i` (SWIG interfaces) also have matching `#ifdef` blocks — those will now take the 8-bit type path, keeping bindings consistent with the runtime layout.
  • `sizeof(gen)` references in `src/global.cc` (archive memory accounting) are unaffected since they consume whatever `sizeof` produces, and that value doesn't change.
  • `src/first.h:434` has a commented-out `//#define GIAC_TYPE_ON_8BITS` inside an `#ifdef USE_GMP_REPLACEMENTS` block — orthogonal, untouched.

Downstream propagation

After this lands on `dev`:

  1. Bump `GIAC_jll` patch version in Yggdrasil G/GIAC (2.0.1 → 2.0.2), pin the source SHA to this PR's merge commit
  2. Bump `libgiac_julia_jll` patch version in Yggdrasil L/libgiac_julia to pick up the new giac headers
  3. Verify in Giac.jl on Windows that `evalf(pi, 50)` returns a `_REAL` instead of a `DOUBLE`, then remove the string-length workaround introduced in Giac.jl#22

Test plan

  • Local meson build on Linux completes
  • Existing giac test suite passes (no test is sensitive to bitfield vs 8-bit; `sizeof(gen)` is preserved)
  • After Yggdrasil propagates, Giac.jl on Windows correctly tags MPFR reals as `_REAL` per the probe data

The historical bitfield layout of class gen:

  unsigned char type:5;
  unsigned char type_unused:3;
  signed char subtype;
  unsigned short reserved;

is laid out and ACCESSED differently across GCC versions. GCC fuses
adjacent bitfield writes (g.type = ...; g.subtype = ...;) into a
single wider store using version-dependent bit placements: GCC 8
and GCC 10 produce instruction sequences that disagree on which
bits of the resulting word hold `type`.

This isn't theoretical. In the libgiac_julia / Giac.jl stack on
Windows:
  - GIAC_jll is built with BinaryBuilder GCC 8
  - libgiac_julia_jll (the CxxWrap wrapper) is GCC 10
  - When libgiac writes a gen tagged _REAL (3) and the wrapper reads
    it, the wrapper sees _DOUBLE_ (1) instead.

A C++ probe (s-celles/libgiac-julia-wrapper#5)
ran on Linux/macOS/Windows in both modes confirmed: with the
bitfield, GCC's optimizer fuses writes; the byte layout is correct
ONLY when libgiac and consumer use the same GCC. With
GIAC_TYPE_ON_8BITS, type is a plain `unsigned char` at offset 0 —
no bitfield, no fusion, every compiler emits the same simple byte
store/load. The ABI becomes compiler-invariant.

Cost: 3 mantissa bits on inline doubles encoded in a gen (48 → 45).
sizeof(gen) is unchanged at 64 bits. Per giac's author this is the
historical default and the safer cross-compiler path. Override by
removing this block in dispatch.h if you need the legacy layout.

Fixes: s-celles/Giac.jl#22
Refs:  s-celles/libgiac-julia-wrapper#5
@s-celles s-celles merged commit 64fdcef into dev May 13, 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