Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Design: Memory-Layout-Compatible Mapping from C/C++ Types to Go

Abierto
#1,029 0 comentarios 0 reacciones 0 asignados Ver en GitHub

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
Stack tecnológico
cpp, go

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

Architecture-Design proposal

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 C and G are struct/class types, then for every field F, offsetof(C, F) == offsetof(G, F) and sizeof(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
  1. 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.
  2. 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.
  3. //llgo:no-padding is 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 just byte when k is 1) right before that field, with k the 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 sizeof because 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
}
  • Allocator is not the last field. If _Alloc is instantiated as an empty class, C++ adds a byte but Go would not. //llgo:padding asks the compiler to insert that byte when the instantiated type is empty.
  • Value needs no directive. If _Tp is 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-padding only where needed for non-template types, and emits //llgo:padding / //llgo:padding-if for 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_paddingN fields, so they stay stable across regeneration.
  • Interaction of //llgo:padding and //llgo:padding-if with 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

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de goplus/llcppg

Todos los issues de goplus/llcppg

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.