diff --git a/cl/compile.go b/cl/compile.go index c0cea9ff..b1db339a 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -216,7 +216,7 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack if conf == nil { conf = &Config{} } - methodCheck, err := newMethodChecks(conf.MethodCheck) + methodCheck, err := newMatchers("MethodCheck", conf.MethodCheck) if err != nil { return } @@ -266,7 +266,6 @@ func NewPackage(pkgPath, pkgName string, files []Source, conf *Config) (ret Pack pkgOf: conf.PackageOf, nameLookup: nameLookup, pubLookup: conf.PubFileLookup, fileBases: make(map[clang.File]int), ovobjs: make(map[string]*overloadObj), macroVals: make(map[string]any), types: make(map[string]typeObj), - logicals: make(map[string]*types.Named), lastSeen: make(map[string]none), impPkgs: make(map[string]none), } diff --git a/cl/ctx.go b/cl/ctx.go index 3a8a157c..1ab7db12 100644 --- a/cl/ctx.go +++ b/cl/ctx.go @@ -120,7 +120,7 @@ type pkgCtx struct { typeAbbrSuffix []string - mthdCheck []*mthdCheck + mthdCheck []*matcher nsPrefix []string fnPrefix []string varPrefix []string @@ -155,8 +155,6 @@ type pkgCtx struct { lastSeen map[string]none // last seen include file set (loaded include files) thisSeen map[string]none // include file set seen in this translation unit - logicals map[string]*types.Named // logical Go class name => logical class type - loads []compileUnit compiles []compileUnit pubs []Entry diff --git a/cl/func.go b/cl/func.go index 2a351420..cd901fcc 100644 --- a/cl/func.go +++ b/cl/func.go @@ -132,9 +132,8 @@ func compileFuncOrMethod(ctx *pkgCtx, obj *overloadObj, this *classCtx) { var nameInPkg string if this == nil { if ctx.lang == LanguageC { - if cls, mayClass := ctx.methodCheck(cName); mayClass { + if cls, obj, mayClass := ctx.methodCheck(pkgTypes, cName); mayClass { // try to method for C global functions - oldParams := params params, recv, typRecv, typName = tryToMethod(ctx, pkgTypes, params) if typRecv != nil { recvCType := fn.Argument(0).Type() @@ -143,18 +142,10 @@ func compileFuncOrMethod(ctx *pkgCtx, obj *overloadObj, this *classCtx) { recvCType = recvCType.Pointee() } typCName = trimTypeTag(clang.String(recvCType.Unqualified())) - // When the function resolves to a logical class distinct from - // the physical receiver type (for example PyList_GetItem - // resolves to List while its receiver is the base class - // Object), emit the method on the logical class - which embeds - // the base - and generate the As conversion method on - // the base class. See logical.go. if isPtr && cls != "" && cls != typName { - if contains(cls, ctx.nonClasses) { - params, recv, typName, typCName = oldParams, nil, "", "" - } else if logical, ok := ctx.logicalClassOf(fn, cls, typRecv, recv); ok { - recv = types.NewParam(recv.Pos(), pkgTypes, recv.Name(), types.NewPointer(logical)) - typRecv = logical + if t := obj.Type(); isTypeFromBase(t, typRecv) { + recv = types.NewParam(recv.Pos(), pkgTypes, recv.Name(), types.NewPointer(t)) + typRecv = types.Unalias(t).(*types.Named) typName = cls } } @@ -220,6 +211,20 @@ func compileFuncOrMethod(ctx *pkgCtx, obj *overloadObj, this *classCtx) { } } +func isTypeFromBase(t types.Type, base *types.Named) bool { + if t, ok := types.Unalias(t).(*types.Named); ok { + if t == base { + return true + } + if s, ok := t.Underlying().(*types.Struct); ok { + if s.NumFields() > 0 { + return isTypeFromBase(s.Field(0).Type(), base) + } + } + } + return false +} + func findMember(typ *types.Named, name string) (pos token.Pos, isField bool, exists bool) { for i := range typ.NumMethods() { m := typ.Method(i) @@ -247,7 +252,7 @@ func tryToMethod(ctx *pkgCtx, pkgTypes *types.Package, params []*types.Var) ([]* first := params[0] t := first.Type() if len(params) == 2 && params[1].Type() == t { - // don't convert to method if the first two params have the same type + // don't convert to method if the two params have the same type return params, nil, nil, "" } tPtr, isPtr := t.(*types.Pointer) diff --git a/cl/logical.go b/cl/logical.go deleted file mode 100644 index 14953852..00000000 --- a/cl/logical.go +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cl - -import ( - "go/token" - "go/types" - - "github.com/goplus/llcppg/clang" -) - -// ----------------------------------------------------------------------------- - -// Logical classes. -// -// MethodCheck can tell which logical type a C function belongs to (for example -// PyList_GetItem belongs to List even though its receiver is the physical base -// class Object). Instead of attaching every such function to the physical base -// class - which pollutes the base API surface and invites method-name conflicts -// (PyList_GetItem and PyDict_GetItem would both want to be Item) - llcppg emits -// each method on its own logical class that embeds the physical base class, plus -// a conversion method on the base class named As that reinterprets the -// pointer. -// -// type List struct { -// Object -// } -// -// func (o *Object) AsList() *List { return (*List)(unsafe.Pointer(o)) } -// -// // llgo:link (*List).Item C.PyList_GetItem -// func (self *List) Item(index *Object) *Object { return nil } -// -// A logical class is generated lazily: only when at least one function actually -// resolves to it. If the logical type is the physical type itself (for example -// PyObject_IsTrue resolves to Object), the method stays on Object and no class -// or AsObject is generated. See https://github.com/goplus/llcppg/issues/948. - -const asMethodPrefix = "As" - -// logicalClassOf returns the logical class named goName whose base is the -// physical type base, creating it (and its As conversion method on base) -// on first use. It is lazy and deterministic: the type and the conversion method -// are emitted in the order functions first resolve to the logical class. -// -// If a package-level type with the same name already exists (generated from the -// headers or provided through TypeAlias), it is reused instead of generating a -// second one, and the As conversion is still added when it is missing. -func (p *pkgCtx) logicalClassOf(decl clang.Cursor, goName string, baseTy *types.Named, baseRecv *types.Var) (*types.Named, bool) { - if lc, ok := p.logicals[goName]; ok { - return lc, lc != nil - } - - named, ok := p.newLogicalType(decl, goName, baseTy) - p.logicals[goName] = named - if !ok { - return nil, false - } - - // genAsMethod runs once per logical class (subsequent resolutions hit the - // cache above). It is attempted whether the type was freshly emitted or an - // existing package-level type was reused, so a reused type still gets its - // As conversion; genAsMethod itself keeps any pre-existing method of - // that name and reports a diagnostic. - p.genAsMethod(decl, named, baseTy, baseRecv) - return named, true -} - -func (p *pkgCtx) newLogicalType(decl clang.Cursor, goName string, base *types.Named) (*types.Named, bool) { - pkg := p.pkg - pkgTypes := pkg.Types - if pkgTypes.Scope().Lookup(goName) != nil { - // can't create a new logical type with the same name as an existing type - // p.ignoref(featExplicitIgnore, decl, "logical class %s: name exists, ignored", goName) - return nil, false - } - typDecl := newType(p, decl, "", goName) - embed := types.NewField(goNodePos(p, decl), pkgTypes, base.Obj().Name(), base, true) - typDecl.InitType(pkg, types.NewStruct([]*types.Var{embed}, nil)) - return typDecl.Type(), true -} - -// genAsMethod emits the conversion method on the base class, for example -// -// func (self *Object) AsList() *List { return (*List)(unsafe.Pointer(self)) } -// -// The conversion only reinterprets the pointer; it does not call into C and does -// not check the object's real type. If a method of the same name already exists -// on the base class, the existing method is kept and a diagnostic is reported. -func (p *pkgCtx) genAsMethod(decl clang.Cursor, named, baseTy *types.Named, baseRecv *types.Var) { - name := asMethodPrefix + named.Obj().Name() - if pos, _, exists := findMember(baseTy, name); exists { - p.errorf(decl, "%s redeclared in this block\n\t%v: other declaration of %s", name, p.position(pos), name) - return - } - - pkg := p.pkg - pkgTypes := pkg.Types - retType := types.NewPointer(named) - results := types.NewTuple(types.NewParam(token.NoPos, pkgTypes, "", retType)) - sig := types.NewSignatureType(baseRecv, nil, nil, nil, results, false) - - f, err := pkg.NewFuncWith(goNodePos(p, decl), name, sig, nil) - if err != nil { - p.panicf(decl, "logical class %s: genAsMethod failed - %v", named.Obj().Name(), err) - } - cb := f.BodyStart(pkg) - // return (*List)(unsafe.Pointer(self)) - cb.Typ(retType). - Typ(p.unsafePointer()).Val(baseRecv). - Call(1). - Call(1). - Return(1).End() -} - -// ----------------------------------------------------------------------------- diff --git a/cl/name.go b/cl/name.go index b6a2a017..38be255a 100644 --- a/cl/name.go +++ b/cl/name.go @@ -18,6 +18,7 @@ package cl import ( "fmt" + "go/types" "strconv" "strings" @@ -43,31 +44,31 @@ func objUSR(decl clang.Cursor) string { // ----------------------------------------------------------------------------- -type mthdCheck struct { +type matcher struct { prefix string pattern []string result string } -func newMethodCheck(check string) (*mthdCheck, error) { - pos := strings.IndexByte(check, ':') +func newMatcher(name, expr string) (*matcher, error) { + pos := strings.IndexByte(expr, ':') if pos < 0 { - return nil, fmt.Errorf("invalid MethodCheck: %s", check) + return nil, fmt.Errorf("invalid %s: %s", name, expr) } - pattern := strings.TrimSpace(check[:pos]) + pattern := strings.TrimSpace(expr[:pos]) parts := strings.Split(pattern, "*") - result := strings.TrimSpace(check[pos+1:]) - return &mthdCheck{ + result := strings.TrimSpace(expr[pos+1:]) + return &matcher{ prefix: parts[0], pattern: parts[1:], result: result, }, nil } -func newMethodChecks(check []string) ([]*mthdCheck, error) { - ret := make([]*mthdCheck, len(check)) - for i, c := range check { - m, err := newMethodCheck(c) +func newMatchers(name string, exprs []string) ([]*matcher, error) { + ret := make([]*matcher, len(exprs)) + for i, c := range exprs { + m, err := newMatcher(name, c) if err != nil { return nil, err } @@ -76,25 +77,26 @@ func newMethodChecks(check []string) ([]*mthdCheck, error) { return ret, nil } -func (p *mthdCheck) check(name string) (cls string, ok bool) { - name, ok = strings.CutPrefix(name, p.prefix) +func (p *matcher) match(source string, matchFull bool) (ret string, matched bool) { + source, ok := strings.CutPrefix(source, p.prefix) if !ok { return } n := 0 match := make([]string, len(p.pattern)) for i, p := range p.pattern { - pos := strings.Index(name, p) + pos := strings.Index(source, p) if pos < 0 { return } n += pos - match[i] = name[:pos] - name = name[pos+len(p):] + match[i] = source[:pos] + source = source[pos+len(p):] } - cls = matchResult(p.result, match, n) - ok = cls != "" - return + if matchFull && source != "" { + return + } + return matchResult(p.result, match, n), true } func matchResult(result string, match []string, n int) string { @@ -119,6 +121,15 @@ func matchResult(result string, match []string, n int) string { return string(b) } +func match(source string, matches []*matcher, matchFull bool) (ret string, matched bool) { + for _, m := range matches { + if r, ok := m.match(source, matchFull); ok { + return r, true + } + } + return "", false +} + // ----------------------------------------------------------------------------- func cNameSplit(cName string) (parts []string, hasNS bool) { @@ -247,15 +258,15 @@ func (p *pkgCtx) typeName(cName string, _ bool) string { return p.cstyleToGo(name, underscoreStart, true) } -func (p *pkgCtx) methodCheck(cName string) (cls string, mayClass bool) { +func (p *pkgCtx) methodCheck(pkgTypes *types.Package, cName string) (cls string, obj types.Object, mayClass bool) { if len(p.mthdCheck) == 0 { - return "", true + return "", nil, true // allow tryToMethod } - for _, m := range p.mthdCheck { - if cls, mayClass = m.check(cName); mayClass { - return - } + cls, mayClass = match(cName, p.mthdCheck, false) + if cls != "" { + obj = pkgTypes.Scope().Lookup(cls) } + mayClass = obj != nil return } diff --git a/tool/_testc/python-3.14.8/include/pythread.h b/tool/_testc/python-3.14.8/include/pythread.h index b265e707..7d7186af 100644 --- a/tool/_testc/python-3.14.8/include/pythread.h +++ b/tool/_testc/python-3.14.8/include/pythread.h @@ -17,7 +17,9 @@ typedef enum Py_Lock_Status { typedef struct _Py_tss_t Py_tss_t; /* opaque */ -struct _object {}; +struct _object { + Py_ssize_t ob_refcnt; +}; typedef struct _object PyObject; @@ -26,9 +28,19 @@ PyAPI_FUNC(int) Py_IsTrue(PyObject *x); PyAPI_FUNC(int) PyObject_IsTrue(PyObject *x); +typedef struct { + struct _object ob_base; + Py_ssize_t ob_size; +} PyListObject; + PyAPI_FUNC(PyObject *) PyList_GetItem(PyObject *x, PyObject *index); PyAPI_FUNC(void) PyList_SetItem(PyObject *x, PyObject *index, PyObject *value); +typedef struct { + struct _object ob_base; + Py_ssize_t ma_used; +} PyDictObject; + PyAPI_FUNC(PyObject *) PyDict_GetItem(PyObject *x, PyObject *index); PyAPI_FUNC(PyObject *) PyImport_Import(PyObject *name); diff --git a/tool/_testc/python-3.14.8/llcppg.cfg b/tool/_testc/python-3.14.8/llcppg.cfg index 31d8fd5f..4ed2ebc8 100644 --- a/tool/_testc/python-3.14.8/llcppg.cfg +++ b/tool/_testc/python-3.14.8/llcppg.cfg @@ -6,9 +6,9 @@ "Files": ["Python.h"], "Deps": [], "Class": [], - "NonClass": ["Import"], + "NonClass": [], "TypePrefix": ["Py_", "Py"], - "MethodCheck": ["Py*_: $1"], + "MethodCheck": ["PyObject_: Object", "Py_: ", "Py*_: $1Object"], "FuncPrefix": ["Py_", "Py"], "MacroPrefix": ["PY_", "Py_"], "EnumPrefix": ["PY_"], diff --git a/tool/_testc/python-3.14.8/pythread.go b/tool/_testc/python-3.14.8/pythread.go index bb67037d..78d2ebaa 100644 --- a/tool/_testc/python-3.14.8/pythread.go +++ b/tool/_testc/python-3.14.8/pythread.go @@ -40,8 +40,17 @@ type X_tssT struct { } type TssT = X_tssT type X_object struct { + ObRefcnt SsizeT } type Object = X_object +type ListObject struct { + ObBase X_object + ObSize SsizeT +} +type DictObject struct { + ObBase X_object + MaUsed SsizeT +} // llgo:type C type MemAllocatorEx struct { @@ -62,33 +71,17 @@ func (self *Object) IsTrue() c.Int { return 0 } -type List struct { - X_object -} - -func (self *Object) AsList() *List { - return (*List)(unsafe.Pointer(self)) -} - -// llgo:link (*List).Item C.PyList_GetItem -func (self *List) Item(index *Object) *Object { +// llgo:link (*ListObject).ListGetItem C.PyList_GetItem +func (self *ListObject) ListGetItem(index *Object) *Object { return nil } -// llgo:link (*List).SetItem C.PyList_SetItem -func (self *List) SetItem(index *Object, value *Object) { +// llgo:link (*ListObject).ListSetItem C.PyList_SetItem +func (self *ListObject) ListSetItem(index *Object, value *Object) { } -type Dict struct { - X_object -} - -func (self *Object) AsDict() *Dict { - return (*Dict)(unsafe.Pointer(self)) -} - -// llgo:link (*Dict).Item C.PyDict_GetItem -func (self *Dict) Item(index *Object) *Object { +// llgo:link (*DictObject).DictGetItem C.PyDict_GetItem +func (self *DictObject) DictGetItem(index *Object) *Object { return nil } @@ -97,9 +90,8 @@ func Import(name *Object) *Object // Get the memory block allocator of the specified domain. // -// llgo:link MemAllocatorDomain.MemGetAllocator C.PyMem_GetAllocator -func (self MemAllocatorDomain) MemGetAllocator(allocator *MemAllocatorEx) { -} +//go:linkname MemGetAllocator C.PyMem_GetAllocator +func MemGetAllocator(domain MemAllocatorDomain, allocator *MemAllocatorEx) // PY_TIMEOUT_MAX is the highest usable value (in microseconds) of PY_TIMEOUT_T // type, and depends on the system threading API.