Proposal: Context-aware zero-value conversion for forwarding macros

Aperta
#1,146 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
25/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
c, cpp, go
Ambito
backend, tooling

Direzione di ricerca

Read the automatic forwarding-macro path from #1109 and the callback failure in #1134, then trace how macro arguments and C/C++ call context reach gogen's ZeroLit. Compare the proposed source-form and parameter-type cases against the current conversion behavior. Done means zero-valued arguments map to equivalent Go zero values while nonzero arguments retain the existing path; the proposal leaves several language and type cases open for design.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

feature proposal

Summary

Make zero-valued constant arguments in automatically detected C and C++ forwarding macros use an equivalent Go zero value when the original argument and the C/C++ call context establish that the values are equivalent. Reuse gogen's ZeroLit for emission after that decision.

The decision starts from the macro argument as written or evaluated under its own C/C++ semantics. It must not turn a nonzero argument into a zero argument merely because conversion to the callee's parameter type would produce zero. For example, an argument 256 remains 256 and follows the existing nonzero-argument path.

This proposal concerns the automatic forwarding-macro path from #1109 and the callback failure in #1134. The explicitly configured MacroFunc C-wrapper path is separate. The tables list cases and boundaries for discussion; listing a case does not claim that the current generator supports it or settle the implementation order.

Motivation

typedef int (*callback_t)(int);
void set_options(int count, int *data, bool enabled, callback_t callback);
#define set_default_options() set_options(0, 0, 0, 0)

The four arguments are all written as 0, but at the call they mean integer zero, a null object pointer, false, and a null function pointer. If the existing Go mappings are c.Int, *c.Int, bool, and a function type, the corresponding call is:

func SetDefaultOptions() {
    SetOptions(0, nil, false, nil)
}

The equivalent C example uses _Bool in place of bool. On the investigated local branch, an ordinary pointer or boolean argument can still reach gogen as an untyped integer and cause generation to panic. The local callback change handles the literal-zero callback case; it is not a general zero-conversion rule.

Proposed rule and boundary

  1. Start with the macro's actual argument. Determine whether it denotes a zero value without first narrowing or otherwise converting a nonzero value to the callee parameter type.
  2. Consider the selected language mode, the argument's source form and type, and its position in the original C/C++ call. A numeric zero is not necessarily a valid null pointer constant, and a Go pointer can represent a C++ reference as well as a C pointer.
  3. When that call is valid and the resulting argument is known to be equivalent to the generated Go parameter's zero value, use the target Go type's zero value. A dedicated internal decision point can reuse ZeroLit for emission; no helper signature or exported API is specified here.
  4. Keep nonzero arguments on the existing nonzero-argument path. The handling of invalid, unsupported, or insufficiently understood cases remains an open question below.
A nonzero value is not a zero-conversion case
void use_byte(unsigned char value);
#define use_large() use_byte(256)

The macro argument is 256, so the zero-conversion decision must leave it as 256. On an 8-bit unsigned char target, C converts this argument to zero when calling use_byte; that target-side narrowing does not make the source argument a zero value for this proposal.

The current Go mapping uses uint8 for unsigned char. An LLGo build of a Go call that passes the constant 256 to uint8 fails with an overflow diagnostic. Preserving the source value therefore does not by itself solve that separate numeric-conversion problem; this proposal does not replace 256 with 0 to avoid the error.

Cases: source argument forms

The table uses void f(int *p); f(ARG); to show whether each form can supply a null int* in the specified language mode. Yes describes C/C++ semantics for the example, not current llcppg support. The comparisons were checked with Clang 22.1.8 in strict modes on macOS/arm64.

Case ARG C17 C23 C++17 Meaning or qualification
1 0 Yes Yes Yes Integer literal zero.
2 0U, 0L, 0x0 Yes Yes Yes Other zero integer literals; suffix and base do not make these nonzero.
3 0b0 No Yes Yes Binary integer literal; accepted in C23 and C++17, not strict C17.
4 (0), ((0)) Yes Yes Yes Parentheses preserve the zero-literal meaning.
5 1 - 1, -0 Yes Yes No A zero-valued integer constant expression; C++17 requires a zero integer literal for this implicit pointer conversion.
6 (int)0 Yes Yes No The C++17 expression is a cast, not an integer literal.
7 '\0' Yes Yes No A zero-valued character constant, but not a C++17 zero integer literal.
8 false Yes, with <stdbool.h> Yes No In C17 the standard macro expands to 0; C23 also accepts false here.
9 enum { ZERO = 0 }; f(ZERO) Yes Yes No A C enum constant can form an integer constant expression; C++17 does not accept this enum expression as a null pointer constant.
10 NULL Yes, when defined as a null pointer constant Yes, when defined Yes, when defined The actual replacement can differ between headers and language modes.
11 #define MY_NULL NULL, then MY_NULL Follows expansion Follows expansion Follows expansion An alias must be understood through its replacement at the use position.
12 nullptr No Yes Yes C23 and C++11 or later support nullptr.
13 A nullptr_t variable holding nullptr Not applicable Yes Yes The checked C23 and C++17 calls accept such a value for int*.
14 __null GNU extension GNU extension GNU extension Clang accepts it for the checked C++17 pointer call; it is not an ISO spelling.
15 (int*)0 Yes Yes Yes A typed null int* value; it is not automatically convertible to an unrelated pointer type.
16 (void*)0 Yes Yes No In C it is a null pointer constant; C++ does not implicitly convert a void* to int*.
17 0.0, -0.0 No No No Floating-point zero is not an implicit null pointer constant.
18 const int z = 0; f(z) No No No A zero-valued integer variable is not accepted as a null pointer constant in these modes.
19 f({}), f({0}) Not applicable Not applicable Yes C++ list initialization at the call position; the target type matters.

For example:

void f(int *p);
#define ZERO_A 0
#define ZERO_B (1 - 1)
#define call_a() f(ZERO_A)
#define call_b() f(ZERO_B)

In C++17, call_a() is valid when expanded, while call_b() is not. The macro definition alone need not be diagnosed until it is expanded in a call. C++98 has different rules: Clang accepts the 1 - 1 pointer call in that mode.

A representative NULL spelling can also depend on the language:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
void set_ptr(int *p);
#define clear_ptr() set_ptr(NULL)

Both checked calls pass a null pointer, so the corresponding Go call for a *c.Int parameter is SetPtr(nil). Other headers can choose other valid definitions of NULL.

Cases: fixed parameter types

The Go expressions in this table describe the expected value only where the source call is valid and the existing C-to-Go mapping makes the values equivalent.

Case C/C++ parameter and source argument Source-call meaning Go value or boundary
1 Integer parameter, 0 Integer zero. 0.
2 Integer parameter, 0U or 0L Integer zero after a valid numeric conversion. 0, if the generated Go mapping preserves that value.
3 C _Bool or C++ bool, 0 False. false.
4 C _Bool or C++ bool, false False. false.
5 C _Bool or C++ bool, 0.0 False after numeric-to-boolean conversion. false.
6 C _Bool or C++ bool, a typed null pointer such as (int*)0 False after pointer-to-boolean conversion. false, when the source conversion is valid.
7 Boolean parameter, nullptr Valid and false in C23; the direct C++17 function argument is invalid. C23 can correspond to false; C++17 must not be treated as a valid call.
8 float or double, 0 or 0.0 Positive floating-point zero. Go numeric zero, where the mapped type represents the same value.
9 float or double, -0.0 Negative floating-point zero; its sign can be observed. Do not assume a positive ZeroLit is equivalent. LLGo's generated Go -0.0 also lost the sign in a local probe, so this requires separate investigation.
10 C double _Complex, 0 Complex zero. The zero of the mapped Go complex type when the mapping is supported.
11 C enum E, 0 A valid integer-to-enum argument in the checked C example. The corresponding generated enum zero.
12 C++ enum E, 0 The implicit function argument is invalid. Do not infer validity from a Go enum's integer underlying type.
13 C++ enum E, an enumerator EZero with value zero A valid value of E. The corresponding enum zero, if the expression is recognized.
14 int*, a valid null pointer argument Null object pointer. nil when mapped to a Go pointer.
15 void*, a valid null pointer argument Null object pointer. nil when mapped to unsafe.Pointer.
16 Function pointer, a valid null pointer argument Null function pointer. nil when mapped to a Go function type.
17 Array parameter int a[3], a valid null pointer argument The parameter behaves as a pointer at the call. nil when mapped to a Go pointer, even if libclang reports an array type for the declaration.
18 typedef void *handle_t, a valid null pointer argument Null pointer represented by a typedef. 0 when the existing mapping emits type HandleT uintptr; nil when NonClass maps it to unsafe.Pointer.
19 int*, (int*)0 Typed null pointer of the expected type. nil when the mapping is a Go pointer.
20 double*, (int*)0 Incompatible pointer argument in the checked C and C++ calls. Do not infer convertibility merely from both pointers being null-valued.
21 int*, (void*)0 Valid null pointer constant in C; invalid implicit void*-to-int* argument in C++. C can correspond to nil; the checked C++ call is invalid.
22 C++ const int& or int&&, 0 Binds a temporary integer whose value is zero. Not nil, even though llcppg maps C++ references to Go pointers.
23 C++ int* const&, 0 Binds a reference to a temporary null pointer. Not a nil reference; the referenced temporary itself contains a null pointer.
24 C++ Box, 0, with a non-explicit Box(int) Calls the converting constructor. Box{} is not generally equivalent; it would not perform that constructor call.
25 C++ pointer-to-member, 0 or nullptr Null data-member or member-function pointer. Do not assume it has the ordinary Go pointer representation; the current mapping needs investigation.
26 C++ std::nullptr_t or C23 nullptr_t, 0 or nullptr A valid null pointer type argument in the checked examples. The current Go mapping and representation need investigation.
27 C struct S, (struct S){0} Aggregate initialization according to C rules. Whether a generated Go S{} is equivalent depends on the mapping.
28 C++ S, S{} or {} Initialization according to the C++ type, possibly including a constructor. A Go composite zero is not automatically equivalent.
29 const char*, "" Pointer to an empty string, not a null pointer. Do not replace the string argument with nil.
Reference and constructor examples
void use_ref(const int &value);
struct Box {
    int value;
    Box(int v) : value(v + 1) {}
};
void use_box(Box value);
#define ref_zero() use_ref(0)
#define box_zero() use_box(0)

ref_zero() binds a temporary integer with value zero; passing a Go nil pointer would not supply that referenced integer. box_zero() calls Box(0), which sets value to 1 in this example. A Go zero composite would not execute the constructor.

Pointer typedef example
typedef void *handle_t;
int use_handle(int marker, handle_t value);
#define use_empty_handle() use_handle(1, 0)

The default typedef mapping produces type HandleT uintptr, for which the corresponding Go call is UseHandle(1, 0). With handle_t configured as NonClass, it maps to unsafe.Pointer and the corresponding call is UseHandle(1, nil). Target and ABI coverage still need validation.

Cases: variadic arguments and macro parameters

int log_value(int count, ...);
#define log_zero() log_value(1, 0)
Case Argument in the variadic portion Meaning or boundary
1 0 An integer argument; the current generated call LogValue(1, 0) must not become LogValue(1, nil).
2 C NULL expanded as (void*)0 A null void* argument, not the same source type as integer 0.
3 C++ NULL expanded as 0 or __null The argument type depends on the actual replacement and compiler behavior.
4 C++ nullptr Clang 22's checked AST converts it to void* for the variadic call.
5 C23 nullptr Clang 22's checked AST retains a nullptr_t expression at this call; the Go/ABI mapping is not established by this observation.

The generated Go signature uses ...any, whose parameter tuple holds a []any type. Passing that slice type to ZeroLit produces nil; it does not describe the individual integer argument in case 1. Source type and default-argument conversions remain relevant in the other cases.

#define call_with_zero() f(0)
#define call_with_arg(x) f(x)

call_with_zero contains a fixed argument available during generation. The value supplied later to call_with_arg is not known during generation and must not be assumed to be zero.

Current implementation and known limits

  • funcCallArg stores a kind and evaluated value, which does not retain enough source-form information to distinguish every case above. In particular, 0 and 1 - 1 can both become integer zero.
  • The local callback-only change consequently emits nil for a C++ callback argument 1 - 1, even though Clang rejects that call when the macro is expanded in C++17.
  • Local probes show ordinary pointer and boolean parameters passed literal 0 can panic during generation. The callback-only path handles literal 0, and integer 0 is preserved.
  • The current expression parser does not fully handle examples including 0U, nullptr, character constants, some aliases of NULL, and typed pointer casts.
  • Automatic lookup requires one matching global function declaration. Overloaded C++ functions and omitted default arguments are not handled by the checked automatic forwarding cases.
  • The checked variadic forwarding path currently compares the number of macro arguments to the number of Go signature parameters; cases with different counts can be skipped.
  • An explicitly configured MacroFunc entry can use a C wrapper where applicable. It requires per-macro configuration and is not the automatic Go-forwarding path addressed here.

Discussion and open questions

  1. Initial supported forms. The tables enumerate the relevant cases; which recognized forms should the first implementation handle, beyond literal 0 and recognized NULL? Examples include suffixed literals, zero-valued macro aliases, nullptr, C integer constant expressions, typed pointer values, and C++ list initialization.
  2. Argument information and language mode. How should the forwarding path retain or obtain enough source form and type information to distinguish 0 from 1 - 1, expand aliases at their use position, and identify the original C/C++ parameter category? How is the actual language standard of the translation unit determined? Typedefs, array parameters, and C functions represented as Go methods need particular care.
  3. Explicit casts within the source argument. f((unsigned char)256) contains a conversion written in the macro itself. On an 8-bit unsigned char target the cast expression has value zero, unlike the uncast source argument 256 in the earlier example. How should this source expression be represented and handled? The decision to keep uncast 256 does not settle this case.
  4. Unsupported or invalid calls. If llcppg cannot establish equivalence, or the expanded call is invalid in the selected language mode, should automatic generation skip the macro or report a diagnostic? No behavior is fixed here.
  5. C++ objects and references. How should temporary references and converting constructors be represented if they are supported later? Their existence must not cause ordinary pointer or composite zero handling to be applied blindly.
  6. Variadic ABI and floating-point representation. How should typed null values be passed through ...any, and how should negative floating-point zero be preserved? long double also has different Go representations on supported targets, so a macOS-only result cannot establish cross-platform behavior.

Validation

  • Cover the source forms and fixed parameter categories above with C and C++ fixture examples, including literal zeros, NULL, aliases, pointer and callback parameters, integers, booleans, typedef mappings, and array parameters.
  • Keep a nonzero example such as 256 on the existing nonzero path; verify that it is not rewritten to a Go zero value. Keep any resulting numeric-conversion failure visible as a separate issue.
  • Cover variadic integer zero and the C++ reference, constructor, member-pointer, and invalid-source boundaries without equating them to ordinary pointer or struct zero values.
  • Compare language-sensitive examples with Clang under the corresponding language mode, and validate affected generator paths using llgo test across the supported environments. Generator experiments so far were on macOS/arm64; Linux generator validation remains outstanding.

Related work

  • #1134: null callback arguments in automatically detected forwarding macros.
  • #1109: automatic forwarding-macro detection and its open question about pointer constants.
  • #1143: conditional macro analysis; retaining replacement-token information is relevant, but preprocessing #if evaluation does not by itself determine C++ function-call conversions.
Lingua principale
Go
Stelle
40
Fork
13
Merge medio
2h 44m
PR unite (30g)
112

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di goplus/llcppg

Tutte le issue di goplus/llcppg

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.