Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package cl

import (
"fmt"
"go/token"
"go/types"
"maps"
Expand Down Expand Up @@ -146,6 +147,9 @@ type Config struct {
// Go const names (optional).
MacroPrefix []string

// Logical-Type Method Detection (optional). See https://github.com/xgo-dev/llcppg/issues/945.
Comment thread
xushiwei marked this conversation as resolved.
MethodCheck []string

// FuncPrefix specifies the prefix to remove from C/C++ global function names when
// generating Go function names (optional).
FuncPrefix []string
Expand Down Expand Up @@ -212,6 +216,10 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack
if conf == nil {
conf = &Config{}
}
methodCheck, err := newMethodChecks(conf.MethodCheck)
if err != nil {
return
}
confGox := &gogen.Config{
Fset: conf.Fset,
Importer: conf.Importer,
Expand Down Expand Up @@ -249,8 +257,8 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack
keepDoc: !conf.DontKeepDoc, stdRecvName: conf.UseStdRecvName, cflags: conf.CFlags,
wrapFileHeader: conf.WrapFileHeader, forceCamelCase: conf.ForceCamelCase,
typeAbbr: conf.TypeAbbr, typeAbbrSuffix: conf.TypeAbbrSuffix,
typePrefix: conf.TypePrefix, typeSuffix: conf.TypeSuffix,
typeAlias: conf.TypeAlias, typeAliasCache: make(map[string]types.Type),
mthdCheck: methodCheck, typePrefix: conf.TypePrefix, typeSuffix: conf.TypeSuffix,
fnPrefix: conf.FuncPrefix, enumPrefix: conf.EnumPrefix, rename: rename,
nsPrefix: conf.NSPrefix, macroPrefix: conf.MacroPrefix, varPrefix: conf.VarPrefix,
nsIgnore: conf.NSIgnore, macroIgnore: conf.MacroIgnore, typeIgnore: conf.TypeIgnore,
Expand All @@ -273,6 +281,10 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack
ret.Package = pkg
ret.Wrap = ctx.wrap
ret.Public = ctx.pubs

if ctx.errCnt > 0 {
err = fmt.Errorf("compilation failed with %d errors", ctx.errCnt)
}
return
}

Expand Down
1 change: 1 addition & 0 deletions cl/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type pkgCtx struct {

typeAbbrSuffix []string

mthdCheck []*mthdCheck
nsPrefix []string
fnPrefix []string
varPrefix []string
Expand Down
24 changes: 16 additions & 8 deletions cl/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,27 +128,35 @@ func compileFuncOrMethod(ctx *pkgCtx, obj *overloadObj, this *classCtx) {

var recv *types.Var
var typRecv *types.Named // if tryToMethod succeeded, this is the recv
var typName, typCName string
var typName, typCName, clsName string
var nameInPkg string
if this == nil {
if ctx.lang == LanguageC {
// try to method for C global functions
params, recv, typRecv, typName = tryToMethod(ctx, pkgTypes, params)
if typRecv != nil {
recvCType := fn.Argument(0).Type()
if recvCType.Kind == lc.Type_Pointer {
recvCType = recvCType.Pointee()
if cls, mayClass := ctx.methodCheck(cName); mayClass {
params, recv, typRecv, typName = tryToMethod(ctx, pkgTypes, params)
if typRecv != nil {
recvCType := fn.Argument(0).Type()
if recvCType.Kind == lc.Type_Pointer {
recvCType = recvCType.Pointee()
}
typCName = trimTypeTag(clang.String(recvCType.Unqualified()))
if cls != "" {
clsName = cls
} else {
clsName = typName
}
}
typCName = trimTypeTag(clang.String(recvCType.Unqualified()))
}
}
} else {
typNamed := this.typNamed
recv = types.NewParam(goNodePos(ctx, fn), pkgTypes, "this", types.NewPointer(typNamed))
typName = typNamed.Obj().Name()
clsName = typName
}

fnName := ctx.funcName(name, obj.order(), typName, typCName, this == nil, true)
fnName := ctx.funcName(name, obj.order(), clsName, typCName, this == nil, true)

if recv == nil {
nameInPkg = fnName
Expand Down
90 changes: 90 additions & 0 deletions cl/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,84 @@ func objUSR(decl clang.Cursor) string {

// -----------------------------------------------------------------------------

type mthdCheck struct {
prefix string
pattern []string
result string
}

func newMethodCheck(check string) (*mthdCheck, error) {
pos := strings.IndexByte(check, ':')
if pos < 0 {
return nil, fmt.Errorf("invalid MethodCheck: %s", check)
}
pattern := strings.TrimSpace(check[:pos])
parts := strings.Split(pattern, "*")
result := strings.TrimSpace(check[pos+1:])
return &mthdCheck{
prefix: parts[0],
pattern: parts[1:],
result: result,
}, nil
}
Comment on lines +52 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Degenerate/empty patterns match everything, no validation

newMethodCheck performs no validation beyond requiring a :. An entry whose prefix is empty (e.g. ":Foo") or that contains adjacent/leading/trailing * (producing empty pattern parts) matches every function name: strings.CutPrefix(name, "") always succeeds and strings.Index(name, "") returns 0. The result is that the static result overrides the inferred typName for all functions, which is almost certainly unintended. Recommend rejecting empty prefix/pattern parts in newMethodCheck so a bad config fails fast.


func newMethodChecks(check []string) ([]*mthdCheck, error) {
ret := make([]*mthdCheck, len(check))
for i, c := range check {
m, err := newMethodCheck(c)
if err != nil {
return nil, err
}
ret[i] = m
}
return ret, nil
}

func (p *mthdCheck) check(name string) (cls string, ok bool) {
name, ok = strings.CutPrefix(name, p.prefix)
if !ok {
return
}
n := 0
match := make([]string, len(p.pattern))
for i, p := range p.pattern {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Range variable p shadows the *mthdCheck receiver in check()

In check(), the range variable p shadows the method receiver p *mthdCheck. It compiles today only because p.result is read outside the loop and the string p is used inside it. This is a maintenance landmine: any future edit referencing a receiver field (p.prefix, p.result) inside the loop body would silently bind to the string element. Rename the range variable (e.g. pat).

pos := strings.Index(name, p)
if pos < 0 {
return
}
n += pos
match[i] = name[:pos]
name = name[pos+len(p):]
}
cls = matchResult(p.result, match, n)
ok = cls != ""
return
}

func matchResult(result string, match []string, n int) string {
b := make([]byte, 0, len(result)+n)
for i := 0; i < len(result); i++ {
if result[i] == '$' {
if i+1 < len(result) {
i++
c := result[i]
if c >= '1' && c <= '9' {
if index := int(c - '1'); index < len(match) {
b = append(b, match[index]...)
continue
}
}
b = append(b, '$', c)
continue
}
}
b = append(b, result[i])
}
return string(b)
Comment on lines +100 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] matchResult silently emits literal $N for out-of-range captures

matchResult passes an out-of-range $N reference through as a literal (e.g. $1 with zero capture groups appends the literal string "$1"). Combined with ok = cls != "", this returns cls="$1", ok=true — a class name that is not a valid Go identifier, producing a broken binding downstream instead of a clear error. A config like "Py_:$1" (no *) triggers this. Consider validating at parse time in newMethodCheck that every $N in result references an existing capture group.

}

// -----------------------------------------------------------------------------

func cNameSplit(cName string) (parts []string, hasNS bool) {
for {
pos := strings.IndexAny(cName, "_:")
Expand Down Expand Up @@ -169,6 +247,18 @@ func (p *pkgCtx) typeName(cName string, _ bool) string {
return p.cstyleToGo(name, underscoreStart, true)
}

func (p *pkgCtx) methodCheck(cName string) (cls string, mayClass bool) {
if len(p.mthdCheck) == 0 {
return "", true
}
for _, m := range p.mthdCheck {
if cls, mayClass = m.check(cName); mayClass {
return
}
}
return
}

func (p *pkgCtx) funcName(cName string, order int, typName, typCName string, global, _ bool) string {
if v, ok := p.rename[cName]; ok {
return v // special case
Expand Down
7 changes: 7 additions & 0 deletions tool/_testc/python-3.14.8/include/pythread.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ typedef enum Py_Lock_Status {

typedef struct _Py_tss_t Py_tss_t; /* opaque */

struct PyObject {};

// Test if an object is the True singleton, the same as "x is True" in Python.
PyAPI_FUNC(int) Py_IsTrue(PyObject *x);

PyAPI_FUNC(int) PyObject_IsTrue(PyObject *x);

#ifndef Py_LIMITED_API
# define Py_CPYTHON_PYTHREAD_H
# include "cpython/pythread.h"
Expand Down
1 change: 1 addition & 0 deletions tool/_testc/python-3.14.8/llcppg.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Class": [],
"NonClass": [],
"TypePrefix": ["Py_", "Py"],
"MethodCheck": ["Py*_: $1"],
"FuncPrefix": ["Py_"],
"MacroPrefix": ["PY_", "Py_"],
"EnumPrefix": ["PY_"],
Expand Down
12 changes: 12 additions & 0 deletions tool/_testc/python-3.14.8/pythread.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tool/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Config struct {
Class []string `json:"Class"` // typedef names to be treated as classes
NonClass []string `json:"NonClass"` // typedef names to be treated as non-classes
NSPrefix []string `json:"NSPrefix"` // C/C++ namespace prefix to remove
MethodCheck []string `json:"MethodCheck"` // C/C++ method check list
FuncPrefix []string `json:"FuncPrefix"` // C/C++ function name prefix to remove
VarPrefix []string `json:"VarPrefix"` // C/C++ variable name prefix to remove
EnumPrefix []string `json:"EnumPrefix"` // C/C++ enum value prefix to remove
Expand Down
1 change: 1 addition & 0 deletions tool/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func (cfg *Config) NewPackage(pkgPath, pkgName, workDir string, index clang.Inde
Class: cfg.Class,
NonClass: cfg.NonClass,
NSPrefix: cfg.NSPrefix,
MethodCheck: cfg.MethodCheck,
FuncPrefix: cfg.FuncPrefix,
VarPrefix: cfg.VarPrefix,
EnumPrefix: cfg.EnumPrefix,
Expand Down
1 change: 1 addition & 0 deletions tool/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func testSingleFile(t *testing.T, idx clang.Index, pkgDir, headerDir, headerFile
Class: conf.Class,
NonClass: conf.NonClass,
NSPrefix: conf.NSPrefix,
MethodCheck: conf.MethodCheck,
FuncPrefix: conf.FuncPrefix,
EnumPrefix: conf.EnumPrefix,
MacroPrefix: conf.MacroPrefix,
Expand Down
Loading