Design: Memory-Layout-Compatible Mapping from C/C++ Types to Go
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Documentación
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Área
- backend, documentation
Línea de trabajo
This issue proposes a design document for C/C++ to Go memory layout mapping, including callbacks, empty structs, templates, and unsupported features. Start by reading the complete issue, especially sections 6–10, which are truncated here and contain implementation constraints and open questions. Done means the design decisions and unresolved questions are reviewed and the document is updated to describe an agreed approach.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
1. Goal
This document describes how C/C++ types are converted into Go types so that the two have identical memory layouts. Let C be a C/C++ type and G its Go counterpart. "Identical layout" means:
sizeof(C) == sizeof(G).alignof(C) == alignof(G).- If
CandGare struct/class types, then for every fieldF,offsetof(C, F) == offsetof(G, F)andsizeof(C.F) == sizeof(G.F).
Layout compatibility is what allows LLGo programs to pass values and pointers across the C/C++ boundary without any marshaling.
Offsets are always taken from the real layout computed by the C++ front end (Clang), for the target ABI. llcppg does not re-derive layout rules by itself. This document assumes the Itanium C++ ABI; other ABIs (for example MSVC, which spells the attribute [[msvc::no_unique_address]]) need their own review.
2. The Common Case: Simple Mappings
For most types the mapping is direct:
| C/C++ | Go |
|---|---|
int, char, unsigned int, ... |
c.Int, c.Char, c.Uint, ... |
T* |
*T |
T[N] |
[N]T |
The rest of this document covers the cases where a direct mapping does not preserve layout or semantics: callbacks and calling conventions, empty structs, and templates that involve empty structs.
3. Callbacks and Calling Conventions
A C/C++ callback is not equivalent to a Go function value or closure. To express C/C++ callbacks in Go, we introduce two directives:
//llgo:cdecl//llgo:stdcall
A directive can annotate either a type or an individual field.
3.1 Annotating a type
When a type carries a calling-convention directive, every callback field in that type that has no directive of its own uses that convention.
//llgo:stdcall
type Callback func()
//llgo:cdecl
type Foo struct {
CallbackA func(data unsafe.Pointer) c.Int
CallbackB func()
}
Here Callback is a stdcall callback type, and both CallbackA and CallbackB in Foo are cdecl callbacks.
3.2 Overriding on a field
A field-level directive takes precedence over the type-level one:
//llgo:cdecl
type Foo struct {
CallbackA func(data unsafe.Pointer) c.Int
//llgo:stdcall
CallbackB func()
}
CallbackA is a cdecl callback (inherited from Foo), while CallbackB is a stdcall callback.
4. Empty Structs
4.1 The problem
Consider this C/C++ code:
struct foo {
};
struct bar {
foo f;
int a, b;
};
C/C++ gives every object a non-zero size, so foo occupies 1 byte, and bar has 1 byte of storage for f followed by alignment padding. Go does not do this for an empty struct that is not the last field, so the layout would diverge.
4.2 Rejected approach: padding inside the empty type
The first idea is to put the padding in the empty type itself:
type Foo struct {
_padding byte
}
This works for plain members, but it breaks as soon as [[no_unique_address]] is involved. When a member is declared with [[no_unique_address]], C++ does not reserve space for it:
struct bar2 {
int a, b;
[[no_unique_address]] foo f;
};
sizeof(bar2) == 8 in C++, but the straightforward Go translation has sizeof == 12, because Go adds a trailing padding byte when the last field of a non-empty struct is zero-sized. Padding that lives inside Foo cannot express "padded here, not padded there", because whether padding exists depends on the use site, not on the type.
4.3 Decision
- An empty C/C++ class maps to an empty Go struct. Padding is never placed inside the empty type. It is added at the point of use.
- llcppg emits an explicit padding field only where Go's own layout would otherwise disagree with the C++ layout. If Go's alignment rules already put the next field at the right offset, no padding field is emitted. The goal is to keep generated structs free of noise.
//llgo:no-paddingis added to a last field of a non-template struct when Go's automatic trailing padding would make the struct larger than in C++.
4.4 How llcppg decides (non-template types)
For each struct, llcppg walks the fields in order, simulating Go's layout alongside the offsets reported by Clang:
- If the next field's Go offset already equals its Clang offset, nothing is emitted.
- If it is smaller, llcppg emits
_llcppg_paddingN [k]byte(or justbytewhenkis 1) right before that field, withkthe missing distance. Since an empty struct field has size 0, the padding byte lands at the same offset as the empty member, which is what C++ does. - At the end, if Go's total size is larger than Clang's
sizeofbecause of trailing padding after a zero-sized last field, the last field gets//llgo:no-padding.
This replaces the old per-position rules and treats plain members and [[no_unique_address]] members the same way. Examples:
// struct foo {};
type Foo struct {}
// struct bar { foo f; int a, b; };
// C++: f at 0 (1 byte), a at 4. Go would put A at 0, so padding is needed.
type Bar struct {
F Foo
_llcppg_padding1 byte
A, B c.Int
}
// struct baz { char c; foo f; int a; };
// C++: c at 0, f at 1, a at 4. Go: C at 0, F at 1, A aligned to 4. No padding needed.
type Baz struct {
C c.Char
F Foo
A c.Int
}
// struct bar1 { [[no_unique_address]] foo f; int a, b; };
// C++: f at 0, a at 0. Go agrees.
type Bar1 struct {
F Foo
A, B c.Int
}
// struct bar2 { int a, b; [[no_unique_address]] foo f; };
// C++ sizeof is 8, Go would add a trailing byte and give 12.
type Bar2 struct {
A, B c.Int
//llgo:no-padding
F Foo
}
4.5 Several empty members of the same type
C++ requires two distinct subobjects of the same type to have different addresses, even with [[no_unique_address]]. So two [[no_unique_address]] members of the same empty type cannot both sit at the same offset; the compiler moves the later one by one byte:
struct foo {};
struct foo2 {};
struct two_same {
[[no_unique_address]] foo a;
[[no_unique_address]] foo b; // cannot share a's address
int x;
};
// a at 0, b at 1, x at 4, sizeof == 8
struct mixed {
[[no_unique_address]] foo a;
[[no_unique_address]] foo2 b; // different type, may share address 0
[[no_unique_address]] foo c; // conflicts with a, not with b
int x;
};
// a at 0, b at 0, c at 1
The rule in 4.4 handles these without special cases, because it follows the real offsets from Clang. The Go output is:
type TwoSame struct {
A Foo
_llcppg_padding1 byte // pushes B to offset 1
B Foo
X c.Int // Go aligns X to 4: no second padding field
}
type Mixed struct {
A Foo
B Foo2
_llcppg_padding1 byte // pushes C to offset 1
C Foo
X c.Int
}
Note that the conflict is with any earlier subobject of the same type at the same offset, not just the member immediately before. In Mixed, c is preceded by b (a different type) but still conflicts with a. Empty base classes count as already placed subobjects too, so an empty base and a first member of the same empty type conflict as well.
4.6 Empty base classes
An empty class used as a base class without conflicts is a special case: C/C++ applies no padding for it (the empty base class optimization), which matches Go's behavior for an embedded empty struct. No padding field and no directive are needed.
struct empty {};
struct derived : empty {
int a, b;
};
// sizeof(derived) == 8
type Empty struct {}
type Derived struct {
Empty
A, B c.Int
}
// unsafe.Sizeof(Derived{}) == 8
Base classes come first in the layout, so an empty base is never the trailing field of a struct that has other members, and Go's trailing-padding rule does not apply. If the derived class has no other members at all, the derived type itself is an empty class and falls under the limitation described in section 9.
4.7 Summary table
| Situation | What llcppg emits |
|---|---|
| Go layout already matches the Clang offset | nothing |
| Go offset is smaller than the Clang offset (empty member before it) | explicit _llcppg_paddingN field before the field |
| Go struct would be larger than C++ because of trailing padding after a zero-sized last field | //llgo:no-padding on the last field |
| Empty base class | embed it, nothing else |
5. Templates and Empty Structs
5.1 The problem
For ordinary structs and classes, llcppg knows at conversion time every member's layout and can emit the padding itself. For template types it cannot, because the template arguments are unknown:
template <typename _Tp, typename _Alloc>
class object {
_Alloc allocator;
_Tp value;
};
_Alloc may or may not be empty, and, with [[no_unique_address]], _Tp and _Alloc may or may not be the same empty type. llcppg cannot decide, so responsibility for producing the correct layout moves to the LLGo compiler, which sees the concrete instantiation. llcppg annotates the Go declaration so the compiler knows what rule to apply.
5.2 Plain members: //llgo:padding
For the template above, the generated Go is:
type Object[_Tp any, _Alloc any] struct {
//llgo:padding
Allocator _Alloc
Value _Tp
}
Allocatoris not the last field. If_Allocis instantiated as an empty class, C++ adds a byte but Go would not.//llgo:paddingasks the compiler to insert that byte when the instantiated type is empty.Valueneeds no directive. If_Tpis empty and is the last field, both C++ and Go add trailing padding, so their behavior already agrees.
Plain members always occupy distinct addresses, so there is no same-type conflict to consider here.
5.3 [[no_unique_address]] members: //llgo:padding-if
If the C++ template declares its members with [[no_unique_address]]:
template <typename _Tp, typename _Alloc>
class object {
[[no_unique_address]] _Alloc allocator;
[[no_unique_address]] _Tp value;
};
the generated Go is:
type Object[_Tp any, _Alloc any] struct {
Allocator _Alloc
//llgo:padding-if
Value _Tp
}
//llgo:padding-if asks the compiler to lay the field out the way C++ does for [[no_unique_address]]:
- No padding by default. An empty field takes no space, and if it is the last field, Go's automatic trailing padding is suppressed.
- Padding on conflict. If the instantiated field type is an empty class, and an already placed subobject of the same type sits at the same offset (an earlier member, or an empty base class), the compiler inserts one byte before the field so it lands at the next address, as C++ does.
This replaces the unconditional //llgo:no-padding for template members, which was only right when the type arguments differed.
In the example, Allocator is the first member and can never conflict, so it needs no directive. Value can conflict with Allocator and may be last, so it carries //llgo:padding-if.
| Instantiation | Allocator |
Value |
Result |
|---|---|---|---|
object<int, Empty> |
offset 0, size 0 | offset 0 | no padding |
object<Empty, Empty> |
offset 0 | conflict, shifted to 1 | one padding byte, sizeof == 2 |
object<Empty, Empty2> |
offset 0 | offset 0 | no padding, no trailing byte |
The conflict test compares instantiated types. Whether it must also look inside nested empty members (an empty class whose own empty subobject has the same type as the neighbor) is an open question (section 10).
5.4 Summary table
| Member kind | Not last field | Last field |
|---|---|---|
| Plain | //llgo:padding (C++ pads, Go does not) |
no directive (both pad) |
[[no_unique_address]] |
no directive, except //llgo:padding-if when it can conflict with an earlier subobject |
//llgo:padding-if (suppress Go's trailing pad, still pad on conflict) |
6. Features Go Does Not Support
Some C/C++ features have no direct Go counterpart, so they cannot be handled by the simple mappings in section 2 or by the padding rules in sections 4 and 5. The layout-compatibility goal from section 1 still applies to them, but each is large enough to need its own design, covered by a separate proposal:
| Feature | Proposals |
|---|---|
union |
#764, #1011 |
| Bit-fields | #770 |
| Virtual methods | #754 |
This document does not repeat the details of those proposals. Where a type containing one of these features is embedded in a larger struct, the layout rules in this document apply to the surrounding fields as usual, and the type's own representation is whatever the corresponding proposal specifies.
7. Directive Reference
| Directive | Applies to | Meaning |
|---|---|---|
//llgo:cdecl |
type or callback field | Callbacks use the cdecl calling convention. On a type, applies to all callback fields without their own directive. |
//llgo:stdcall |
type or callback field | Same as above, with the stdcall convention. |
//llgo:padding |
field of a generic struct | If the field's instantiated type is empty, the compiler inserts C++-style padding after it (plain members). |
//llgo:padding-if |
field of a generic struct | Lay the field out like a C++ [[no_unique_address]] member: no padding by default, no Go trailing padding if last, and one padding byte when the instantiated type is an empty class that conflicts with an already placed subobject of the same type at the same offset. |
//llgo:no-padding |
last field of a non-template struct | Suppress Go's automatic trailing padding for a zero-sized last field, matching C++ [[no_unique_address]]. |
Non-template structs do not use //llgo:padding or //llgo:padding-if. llcppg emits explicit _llcppg_paddingN fields instead, only where Go's own layout would not match.
8. Responsibilities
- llcppg takes offsets and sizes from Clang, emits explicit padding fields and
//llgo:no-paddingonly where needed for non-template types, and emits//llgo:padding///llgo:padding-iffor template types. - LLGo compiler honors the directives on instantiated generic types and enforces the layout rules above, so
sizeof, alignment and field offsets match the C++ instantiation. - Layout test package (see section 9.1) verifies the result for every converted type.
9. Known Limitation: Standalone Empty Classes
Because an empty C/C++ class maps to an empty Go struct (section 4.3), the two languages disagree about its size when the type is used on its own:
struct foo {};
// sizeof(foo) == 1
type Foo struct {}
// unsafe.Sizeof(Foo{}) == 0
This is a deliberate trade-off. Padding is applied at the use site rather than inside the type, so sizeof(C) == sizeof(G) cannot hold for a bare empty class. It still holds for every non-empty struct that contains one, because the padding rules in sections 4 and 5 keep those layouts and field offsets identical.
In practice this mismatch is harmless. An empty class carries no data, so code that only passes it around, embeds it, or takes a pointer to it behaves the same on both sides. The difference can only show up in code that depends on the size of the empty type itself, such as pointer arithmetic or indexing over an array of empty objects (the stride is 1 in C++ and 0 in Go). Such code is rare. This is recorded here as a known limitation, and no mitigation is planned for now.
10. Layout verification
llcppg generates a layout test package alongside each converted package. It compares size, alignment, field offsets and field sizes against values computed by a C++ compiler, and fails with a clear message on any mismatch. The standalone empty class case is recorded as an expected exception (C++ 1, Go 0). Details are in the separate proposal (#1030).
11. Open Questions
- Naming and numbering scheme for generated
_llcppg_paddingNfields, so they stay stable across regeneration. - Interaction of
//llgo:paddingand//llgo:padding-ifwith templates whose arguments are themselves instantiations of templates with empty members, including nested empty subobjects of the same type at the same offset. - Whether the compiler should also verify layouts at build time, in addition to the generated test package.
- ABI coverage beyond Itanium (MSVC
[[msvc::no_unique_address]]).
- Lenguaje dominante
- Go
- Estrellas
- 39
- Forks
- 13
- Merge medio
- 1 h 33 min
- PR fusionados (30 d)
- 256
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de goplus/llcppg
-
Dificultad 3/5 1-2 días Aptitud para principiantes 35/100
Los mantenedores suelen responder en 1 día
-
configuration
Dificultad 4/5 3-5 días Aptitud para principiantes 30/100
Los mantenedores suelen responder en 1 día
-
Architecture-Design feature proposal
Dificultad 5/5 Más de una semana Aptitud para principiantes 15/100
goplus/llcppg#1056 · 6 comentarios ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 5/5 Más de una semana Aptitud para principiantes 28/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
Los mantenedores suelen responder en 1 día
Todos los issues de goplus/llcppg
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
keyxmakerx/Chronicle#1469 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Gentleman-Programming/gentle-ai#5565 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug frontend grammar sev-3
Dificultad 2/5 1-3 horas Aptitud para principiantes 63/100
-
Docs: say where source and device come from in the Withings and Polar guidesPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertoconnector docs good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
soma-intelligence/open-health-sync#134 ·
Los mantenedores suelen responder en 1 día
-
bug frontend
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 3 días