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
1 change: 1 addition & 0 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ 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]*logicalClass),
lastSeen: make(map[string]none), impPkgs: make(map[string]none),
}

Expand Down
2 changes: 2 additions & 0 deletions cl/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ 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]*logicalClass // logical Go class name => logical class info

loads []compileUnit
compiles []compileUnit
pubs []Entry
Expand Down
12 changes: 12 additions & 0 deletions cl/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ func compileFuncOrMethod(ctx *pkgCtx, obj *overloadObj, this *classCtx) {
} else {
clsName = typName
}
// 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<Class> conversion method on
// the base class. See logical.go.
if cls != "" && cls != typName {
logical := ctx.logicalClassOf(fn, cls, typRecv)
recv = types.NewParam(recv.Pos(), pkgTypes, recv.Name(), types.NewPointer(logical))
typRecv = logical
typName = cls
}
}
}
}
Expand Down
141 changes: 141 additions & 0 deletions cl/logical.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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<Class> 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"

type logicalClass struct {
named *types.Named // the logical class type (List)
base *types.Named // the physical base class it embeds (Object)
}

// logicalClassOf returns the logical class named goName whose base is the
// physical type base, creating it (and its As<Class> 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<Class> conversion is still added when it is missing.
func (p *pkgCtx) logicalClassOf(decl clang.Cursor, goName string, base *types.Named) *types.Named {
if lc, ok := p.logicals[goName]; ok {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Logical-class cache ignores base; differing receiver silently absorbed

logicalClassOf caches by goName only (p.logicals[goName]). If two C functions resolve via MethodCheck to the same logical class name but have different physical receiver types, the second reuses the first's logical class (which embeds the first function's base) with no diagnostic. At the call site in func.go the receiver is then unconditionally rewritten to that cached logical type, so the emitted method would embed/convert through the wrong base relative to its actual C receiver. Latent given typical MethodCheck patterns, but there is no guard — consider keying on (goName, base) or reporting a diagnostic when p.logicals[goName].base != base.

return lc.named
}

named := p.newLogicalType(decl, goName, base)
lc := &logicalClass{named: named, base: base}
p.logicals[goName] = lc

// 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<Class> conversion; genAsMethod itself keeps any pre-existing method of
// that name and reports a diagnostic.
Comment on lines +80 to +81

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Doc comment calls a hard compilation error a soft "diagnostic"

The comment says "genAsMethod itself keeps any pre-existing method of that name and reports a diagnostic" (also lines 66-67 and 111-112). In practice genAsMethod calls p.errorf(...) on a name collision, which increments p.errCnt; compile.go:287 then turns any errCnt > 0 into fmt.Errorf("compilation failed with %d errors", ...), and under FailFast checkFailFast can os.Exit(1). So the outcome is a compilation error that fails generation (the As<Class> method is simply skipped), not a tolerant warning. Only the narrow "existing method is kept" sub-claim is true. Suggest rewording to state the collision is reported as a compilation error and the conversion is skipped.

p.genAsMethod(decl, lc)
return named
}

// newLogicalType returns the named type for the logical class, reusing an
// existing package-level type when one is present and otherwise emitting a fresh
// "type <goName> struct { <base> }".
func (p *pkgCtx) newLogicalType(decl clang.Cursor, goName string, base *types.Named) *types.Named {
if o := p.pkg.Types.Scope().Lookup(goName); o != nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Reused pre-existing type not verified to embed base at offset 0

newLogicalType reuses any package-level type found by Scope().Lookup(goName) (e.g. one from headers or TypeAlias) without checking it is a struct whose first field is the embedded base. genAsMethod then unconditionally emits return (*List)(unsafe.Pointer(o)). That reinterpret cast is only memory-safe when the reused type actually begins with the base layout; the freshly generated path guarantees this (single embedded field at offset 0), but the reuse path does not. A same-named type with a different layout would produce a silently incorrect, memory-unsafe conversion. Recommend validating the reused type's first field is base (skip/diagnose otherwise) before adding the conversion, and documenting the layout assumption.

if tn, ok := o.(*types.TypeName); ok {
if n, ok := tn.Type().(*types.Named); ok {
return n
}
}
}

pkg := p.pkg
pkgTypes := pkg.Types
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()
}

// genAsMethod emits the conversion method on the base class, for example
//
// func (o *Object) AsList() *List { return (*List)(unsafe.Pointer(o)) }
//
// 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, lc *logicalClass) {
name := asMethodPrefix + lc.named.Obj().Name()
if pos, _, exists := findMember(lc.base, 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
recvType := types.NewPointer(lc.base)
retType := types.NewPointer(lc.named)
recv := types.NewParam(goNodePos(p, decl), pkgTypes, "o", recvType)
results := types.NewTuple(types.NewParam(token.NoPos, pkgTypes, "", retType))
sig := types.NewSignatureType(recv, nil, nil, nil, results, false)

f, err := pkg.NewFuncWith(goNodePos(p, decl), name, sig, nil)
if err != nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P3] genAsMethod aborts whole run via panicf instead of errorf

On a NewFuncWith error, genAsMethod calls p.panicf (→ log.Panicf), tearing down the entire generation run. Note findMember only checks the base's own methods/direct fields, not names promoted through the base's embedded types, so a promoted-name collision would fall through to NewFuncWith and hit this panic rather than the graceful errorf path above it. For consistency with the rest of the file's error handling, consider errorf + return so one malformed/colliding symbol doesn't abort the whole package.

p.panicf(decl, "logical class %s: genAsMethod failed - %v", lc.named.Obj().Name(), err)
}
cb := f.BodyStart(pkg)
// return (*List)(unsafe.Pointer(o))
cb.Typ(retType).
Typ(p.unsafePointer()).VarVal("o").
Call(1).
Call(1).
Return(1).End()
}

// -----------------------------------------------------------------------------
2 changes: 2 additions & 0 deletions tool/_testc/python-3.14.8/include/pythread.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ PyAPI_FUNC(int) PyObject_IsTrue(PyObject *x);

PyAPI_FUNC(PyObject *) PyList_GetItem(PyObject *x, PyObject *index);

PyAPI_FUNC(PyObject *) PyDict_GetItem(PyObject *x, PyObject *index);

#ifndef Py_LIMITED_API
# define Py_CPYTHON_PYTHREAD_H
# include "cpython/pythread.h"
Expand Down
29 changes: 25 additions & 4 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.

Loading