Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Proposal: `MacroFunc` Automatic Detection of Forwarding Macros

Đã đóng
#1,109 2 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@fennoai đang làm issue này rồi.

Từ ngày 26/10/2026.

  • #1110 của @fennoai — đang mở

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
35/100
Loại issue
Tính năng
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
go
Lĩnh vực
tooling

Hướng nghiên cứu

Read the base MacroFunc proposal in xgo-dev/llcppg#1051 and trace how llcppg parses function-like macros, resolves generated functions, and emits Go declarations. Implement automatic detection for the forwarding-call cases and add tests for the stated detection rules, constant handling, sizeof translation, and name collisions. Done means qualifying macros generate the described Go functions, while nonqualifying macros are skipped with the specified verbose diagnostics.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Architecture-Design configuration feature

This proposal builds on the MacroFunc option described in xgo-dev/llcppg#1051. It adds an automatic detection mode: function-like macros whose body is a plain forwarding call are recognized on their own, their reference function is inferred, and they are turned into ordinary Go functions with no C wrapper and no configuration.

Summary

In the base MacroFunc proposal, every entry is written by the user as a pair of macro name and reference function, and the result is always a C wrapper plus a //go:linkname declaration. That is the right tool for macros with real logic, such as z_gzgetc, and it is unchanged by this proposal.

Many macros are much simpler: they just call a real function and fill in some of its arguments. For these, llcppg can see everything it needs in the macro body:

  • the reference function is the function being called, so it can be inferred;
  • the parameter types come from that function's prototype;
  • the filled-in arguments are constants, which llcppg either evaluates itself (such as ZLIB_VERSION) or translates to Go (sizeof).

llcppg therefore detects such macros automatically and generates a Go function whose body is one call. With this, the zlib binding needs only the macro that really requires a wrapper:

{
  "MacroFunc": {
    "z_gzgetc": "gzgetc_"
  }
}

z_gzgetc is wrapped through C as before (explicit entry, explicit reference function). z_deflateInit, z_inflateInit, z_deflateInit2, z_inflateInit2 and z_inflateBackInit are detected automatically and need no entry.

Motivation

The five zlib init macros are the typical case:

#  define z_deflateInit(strm, level) \
          deflateInit_((strm), (level), ZLIB_VERSION, (int)sizeof(z_stream))
#  define z_inflateInit(strm) \
          inflateInit_((strm), ZLIB_VERSION, (int)sizeof(z_stream))
#  define z_deflateInit2(strm, level, method, windowBits, memLevel, strategy) \
          deflateInit2_((strm),(level),(method),(windowBits),(memLevel),\
                        (strategy), ZLIB_VERSION, (int)sizeof(z_stream))
#  define z_inflateInit2(strm, windowBits) \
          inflateInit2_((strm), (windowBits), ZLIB_VERSION, \
                        (int)sizeof(z_stream))
#  define z_inflateBackInit(strm, windowBits, window) \
          inflateBackInit_((strm), (windowBits), (window), \
                           ZLIB_VERSION, (int)sizeof(z_stream))

Each one is a call to a known function in which every argument is either one of the macro's own parameters or a compile-time constant. Writing a MacroFunc entry for each would only repeat what the macro body already says (z_deflateInit → deflateInit_), and the C wrapper would contain nothing but the same call that Go can make directly. Detecting these macros automatically removes one config line per macro and avoids an extra C symbol per macro.

Configuration

None. Automatic detection is always on and applies to every function-like macro that satisfies the rules below. A macro listed in MacroFunc is processed in wrap mode, exactly as in the base proposal (reference function given by the user), and is never touched by automatic detection.

Detection rules

A function-like macro is detected when all of the following hold. Whitespace and redundant parentheses around the whole body, the callee name and individual arguments are ignored.

1. The body is a single function call. The callee is a plain identifier (optionally parenthesized, as in (gzgetc_)(g)). Conditional expressions, comma expressions, member access, nested calls, casts around the whole call and multi-statement bodies are not detected. This is what keeps z_gzgetc out of the automatic set.

2. The callee is a function that llcppg has parsed and will generate. It must be a non-variadic function declared in the processed headers and present in the generated Go package. A callee that is itself a macro, a function pointer variable, or a function excluded from generation does not qualify. The callee found here is the macro's reference function; the user never states it.

3. Every argument is either a macro parameter or a constant.

  • A macro parameter is an argument that is exactly one of the macro's parameters (for example (strm)).
  • A constant is any argument that does not mention a macro parameter and is one of:
    • a value llcppg can evaluate at generation time to an integer, floating-point or string value: literals, object-like macros with constant values (ZLIB_VERSION), and casts and arithmetic over these;
    • a sizeof(T) expression where T is a type that llcppg generates as a complete Go type (sizeof(z_stream)), optionally cast to a scalar type ((int)sizeof(z_stream)) or combined with other constants in arithmetic.

4. Every macro parameter is passed exactly once. A parameter that never appears cannot get a type, and a parameter that appears several times would be evaluated several times by the macro but only once by a Go function, which changes behavior when arguments have side effects. Both cases are left alone.

Macros that fail any rule are not an error. They fall through to the existing behavior (currently: not exposed), and can still be listed in MacroFunc if the user wants a C wrapper. When llcppg runs verbosely it prints one line per skipped candidate with the rule that failed.

Semantics

Parameter types. Each macro parameter is passed straight to one callee parameter, so its type is the type of that callee parameter. This is simpler than the explicit form, which has to match macro parameters to reference-function parameters by name and then by position. The macro's own parameter names are kept in the Go signature.

Return type. The Go function returns whatever the callee returns. If the callee returns void, the Go function has no result and the body is a bare call.

Constants. Constants are handled in one of two ways, depending on whether their value depends on the platform:

  • Platform-independent constants (literals, constant macros such as ZLIB_VERSION, and casts and arithmetic over them) are evaluated by llcppg during generation, and the Go code contains the resulting value directly.
  • sizeof(T) is not evaluated. It is translated to unsafe.Sizeof of the corresponding generated Go type, so the value is computed by the Go compiler for whichever platform the binding is built on. A cast around it becomes a Go conversion to the type produced by the normal type conversion pipeline ((int)sizeof(z_stream) becomes c.Int(unsafe.Sizeof(ZStream{}))). When no cast is present, the result is still converted to the callee's parameter type, because unsafe.Sizeof yields a uintptr.

A constant that does not fit the callee's parameter type (for example a value too large for a c.Int) fails rule 3, and the macro is skipped.

Generated code

For the zlib examples:

func DeflateInit(strm *ZStream, level c.Int) c.Int {
	return DeflateInit_(strm, level, c.Str("1.3.1"), c.Int(unsafe.Sizeof(ZStream{})))
}

func InflateInit(strm *ZStream) c.Int {
	return InflateInit_(strm, c.Str("1.3.1"), c.Int(unsafe.Sizeof(ZStream{})))
}

func InflateInit2(strm *ZStream, windowBits c.Int) c.Int {
	return InflateInit2_(strm, windowBits, c.Str("1.3.1"), c.Int(unsafe.Sizeof(ZStream{})))
}

func InflateBackInit(strm *ZStream, windowBits c.Int, window *c.Uchar) c.Int {
	return InflateBackInit_(strm, windowBits, window, c.Str("1.3.1"), c.Int(unsafe.Sizeof(ZStream{})))
}

How each argument form is translated:

C argument Go argument
macro parameter p p
numeric constant (literal, constant macro, cast or arithmetic over them) the evaluated value as an untyped Go constant
string constant (string literal, string-valued macro) c.Str("...") with the evaluated string
sizeof(T) unsafe.Sizeof(T{}), converted to the callee's parameter type (or to the cast type, if the macro has one)

For types where a composite literal is not available (pointers, arrays, scalar typedefs), the Go side uses an equivalent zero-value expression of the same type.

The Go function name is derived from the macro name with the existing naming rules (FuncPrefix, ForceXXX, NamePrefix stripping and so on), exactly as for a regular function. The callee keeps its own generated Go declaration next to it.

Why sizeof goes through unsafe.Sizeof

sizeof(z_stream) differs between platforms (for example 32-bit and 64-bit targets). Computing it in Go means the generated source does not bake in the layout of the machine that ran llcppg, and one generated binding stays correct on every platform the Go struct is valid for. This relies on the generated Go struct having the same layout as the C struct, which llcppg already guarantees for every struct it emits (it is what makes passing *ZStream to C correct in the first place). Types that llcppg cannot emit as complete types, such as opaque or incomplete structs, do not satisfy rule 3, so macros using sizeof on them are not detected.

Documentation

The macro's own doc comment is used if the #define has one. Otherwise the doc comment of the callee's declaration is used. If neither exists, the generated function has no doc comment. The two are never merged, and the leading name of the Go doc comment is the Go name of the macro-derived function, not of the callee.

Name collisions

The Go name derived from a macro can coincide with the Go name of another generated symbol. In zlib this is a real possibility: with underscore stripping enabled, deflateInit_ and z_deflateInit can both map to DeflateInit. When detection finds such a collision it does not emit the function. It logs a warning naming the macro and the clashing symbol, and the user can resolve the clash through the existing naming options. An explicit MacroFunc entry that collides is a hard error, as in the base proposal.

Relationship to the explicit form

Explicit MacroFunc entry Automatically detected
Enabled by an entry in the MacroFunc map nothing, always on
Reference function written by the user inferred: the callee in the macro body
Parameter types name match, then position match against the reference function read from the callee prototype
Return type reference function's callee's
Implementation C wrapper + //go:linkname plain Go function calling the callee, constants filled in
Macro body anything the C compiler accepts a single forwarding call
Failure mode hard error silently skipped (logged in verbose mode)

Automatic detection handles the simple case. Anything it does not recognize can still be handled by the explicit form, and a macro listed explicitly is never reinterpreted.

Design rationale and alternatives

Why direct Go calls instead of C wrappers? For a forwarding macro, the C wrapper would contain nothing but the same call the Go function can make itself. Skipping it avoids an extra exported symbol per macro and an extra layer of calls. The Go function is also visible to the Go compiler for inlining and to tooling such as go doc as an ordinary function.

Why can the reference function be inferred here but not in the explicit form? In a forwarding macro the whole body is the call, so there is exactly one candidate and no ambiguity. In a complex macro the body can mention several functions, and which one defines the wrapper's contract is a decision for the user, so the explicit form keeps requiring it.

Why evaluate some constants in llcppg but translate sizeof? Values such as ZLIB_VERSION are the same on every platform, so writing the evaluated value keeps the generated Go a plain call with literal arguments. sizeof is the one common constant that is not platform-independent, so it is left to the Go compiler through unsafe.Sizeof, which gives the binding wider platform compatibility at the cost of one unsafe import.

Why a syntactic rule instead of general expression translation? The rule set (one call, arguments that are parameters or constants) is small and predictable, and it covers the real macros that motivated the feature. Translating arbitrary C expressions would need a C expression type checker.

Why require each parameter to appear exactly once? A macro substitutes its argument text each time the parameter appears, while a Go function evaluates its argument once. Restricting to a single use removes the only observable difference between the macro and the generated function.

Why no switch? The rules are narrow, so a macro that matches is by construction a thin forwarding layer over a function that is already part of the binding. These macros are usually the intended entry points of the library (the zlib *Init macros are the documented way to start a stream), so exposing them is what users expect. A switch would only turn a per-macro burden into a global flag every binding author must discover.

Why not fail on non-matching macros? The user has not named any macro, so a macro that does not fit the pattern is the normal case, not a configuration mistake.

Compatibility

Because detection is always on, regenerating an existing binding can add new Go functions: every macro that satisfies the rules now appears in the output, and files that use sizeof gain an unsafe import. Nothing that was generated before is changed or removed, and no C wrapper code is produced for detected macros. Bindings that contain no matching macros generate exactly the same output as before. Existing MacroFunc entries behave exactly as in the base proposal.

Open questions

  1. Opt-out. If a library has a matching macro that should not be exposed, what is the best way to exclude it? One option is a marker value in the MacroFunc map, such as "z_foo": "-". Another is a dedicated exclusion list.
  2. Other platform-dependent constants. sizeof is translated because its value varies by platform. Are there other constant forms in real headers (for example alignof or offsetof) that should get the same treatment instead of being evaluated by llcppg?
  3. Pointer constants. Should arguments such as NULL or (void *)0 count as constants, and be emitted as nil? The first version only handles integer, floating-point and string constants and sizeof.
  4. Macros calling macros. A macro whose body calls another detected macro could be chained. This is not supported in the first version.
  5. Alignment with the unified name matcher. If the explicit form later gains wildcard keys, wildcard rules and automatic detection should agree on which one wins when both apply to the same macro. The current rule, explicit entries first, is expected to carry over.
Ngôn ngữ chính
Go
Star
41
Fork
13
Merge trung bình
2 giờ 27 phút
Pull request đã merge (30 ngày)
168

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của goplus/llcppg

Tất cả issue của goplus/llcppg

Issue tương tự

Thêm issue về Go

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.