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
5 changes: 0 additions & 5 deletions cl/_mod/go.mod

This file was deleted.

2 changes: 0 additions & 2 deletions cl/_mod/go.sum

This file was deleted.

3 changes: 0 additions & 3 deletions cl/_mod/stub.go

This file was deleted.

2 changes: 1 addition & 1 deletion cl/_testmockc/function/in.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
unsigned f(int a);

void g();
void _g();

signed int xprintf(const char* fmt, ...);
16 changes: 12 additions & 4 deletions cl/_testmockc/function/out.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package foo

import "github.com/goplus/lib/c"
import (
"github.com/goplus/lib/c"
_ "unsafe"
)

func f(a c.Int) c.Uint
func g()
func xprintf(fmt *c.Char, __llgo_va_list ...any) c.Int
//go:linkname F C.f
func F(a c.Int) c.Uint

//go:linkname X_g C._g
func X_g()

//go:linkname Xprintf C.xprintf
func Xprintf(fmt *c.Char, __llgo_va_list ...any) c.Int
29 changes: 27 additions & 2 deletions cl/blockctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ type blockCtx struct {
fset *token.FileSet
file *token.File
c gogen.PkgRef

nameLookup func(manglingName string) (archivePath string, ok bool)

unsafeImported bool
}

func (p *blockCtx) forceImportUnsafe() {
if !p.unsafeImported {
p.unsafeImported = true
p.pkg.ForceImport("unsafe")
}
}

func (p *blockCtx) initFile(file Source) {
Expand All @@ -89,9 +100,23 @@ func (p *blockCtx) initFile(file Source) {
}

func (p *blockCtx) getPubName(pfnName *string) (rewritten bool) {
// TODO(xsw):
_ = pfnName
fnName := *pfnName
pubName := cPubName(fnName)
rewritten = fnName != pubName
if rewritten {
*pfnName = pubName
}
return
}

func cPubName(name string) string {
if r := name[0]; 'a' <= r && r <= 'z' {

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] cPubName indexes name[0] with no empty-string guard

cPubName reads name[0] without checking for an empty string; name == "" panics with index-out-of-range. fnName originates from clang.String(fn) on parsed headers, so an anonymous/empty name (or an unexpected clang result) triggers the panic via getPubName. Cheap to guard: if name == "" { return name } at the top. (The ASCII byte-arithmetic uppercasing and the X prefix for leading _ are otherwise correct and match the expected output.)

r -= 'a' - 'A'
return string(r) + name[1:]
} else if r == '_' {
return "X" + name
}
return name
}

// -----------------------------------------------------------------------------
8 changes: 4 additions & 4 deletions cl/cltest/cltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (

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

// TestFromDir runs testFunc for each subdirectory of relDir. If sel is not empty, only subdirectories
// whose path contains sel will be tested.
// TestFromDir runs testFunc for each subdirectory of relDir. If sel is not empty, only
// subdirectories whose path contains sel will be tested.
func TestFromDir(t *testing.T, sel, relDir string, testFunc func(t *testing.T, pkgDir string)) {
dir, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -54,8 +54,8 @@ func TestFromDir(t *testing.T, sel, relDir string, testFunc func(t *testing.T, p

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

// MockNameLookup is a mock implementation of the NameLookup function. It returns a fixed archive
// path and true for any input.
// MockNameLookup is a mock implementation of the NameLookup function. It returns a
// fixed archive path and true for any input.
func MockNameLookup(manglingName string) (archivePath string, ok bool) {
return "libfoo.a", true
}
Expand Down
158 changes: 24 additions & 134 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package cl

import (
"go/ast"
"go/token"
"go/types"
"log"
Expand Down Expand Up @@ -55,7 +56,8 @@ type Package struct {
pi *PkgInfo
}

// Reused specifies to reuse the Package instance between processing multiple C/C++ header files.
// Reused specifies to reuse the Package instance between processing multiple C/C++
// header files.
type Reused struct {
pkg Package
}
Expand All @@ -74,10 +76,12 @@ type Config struct {
// Include specifies include searching directories.
Include []string

// Reused specifies to reuse the Package instance between processing multiple C/C++ header files.
// Reused specifies to reuse the Package instance between processing multiple C/C++
// header files.
*Reused

// NameLookup looks up the archive path for a given mangling name. It returns the archive path and a boolean indicating whether the lookup was successful.
// NameLookup looks up the archive path for a given mangling name. It returns the
// archive path and a boolean indicating whether the lookup was successful.
NameLookup func(manglingName string) (archivePath string, ok bool)
}

Expand Down Expand Up @@ -127,9 +131,9 @@ func loadFile(p *gogen.Package, conf *Config, file Source) (pi *PkgInfo, err err
c := p.Import("github.com/goplus/lib/c")
ctx := &blockCtx{
pkg: p, cb: p.CB(), fset: p.Fset, c: c,
nameLookup: conf.NameLookup,
}
ctx.initFile(file)
_ = conf
clang.VisitChildren(file.TU.Cursor(), func(decl, parent clang.Cursor) clang.ChildVisitResult {
compileDecl(ctx, decl)
return clang.Continue
Expand All @@ -150,52 +154,7 @@ func compileDecl(ctx *blockCtx, decl clang.Cursor) {
case lc.CursorVarDecl:
// compileVarDecl(ctx, decl, global)
case lc.CursorTypedefDecl:
/* origName, pub := decl.Name, false
if global {
pub = ctx.getPubName(&decl.Name)
}
compileTypedef(ctx, decl, global, pub)
if pub {
substObj(ctx.pkg.Types, scope, origName, scope.Lookup(decl.Name))
}
case ast.RecordDecl:
pub := false
name, suKind := ctx.getSuName(decl, decl.TagUsed)
origName := name
if global {
if suKind == suAnonymous {
// pub = true if this is a public typedef
pub = i+1 < n && isPubTypedef(ctx, node.Inner[i+1])
} else {
pub = ctx.getPubName(&name)
if decl.CompleteDefinition && ctx.checkExists(name) {
continue
}
}
}
typ, del := compileStructOrUnion(ctx, name, decl, pub)
if suKind != suAnonymous {
if pub {
substObj(ctx.pkg.Types, scope, origName, scope.Lookup(name))
}
break
}
ctx.unnameds[decl.ID] = unnamedType{typ: typ, del: del}
for i+1 < n {
next := node.Inner[i+1]
if next.Kind == ast.VarDecl {
if ret, ok := checkAnonymous(ctx, scope, typ, next); ok {
compileVarWith(ctx, ret, next)
i++
continue
}
}
break
}
case ast.EmptyDecl:
case ast.StaticAssertDecl:
continue
*/
// TODO(xsw)
case lc.CursorEnumDecl:
// compileEnum(ctx, decl, global)
default:
Expand All @@ -205,6 +164,14 @@ func compileDecl(ctx *blockCtx, decl clang.Cursor) {

// TODO(xsw): method support
func compileFunc(ctx *blockCtx, fn clang.Cursor) {
manglingName := clang.Mangling(fn)
if _, ok := ctx.nameLookup(manglingName); !ok {

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.

[P1] nil NameLookup panics compileFunc for callers that omit it

ctx.nameLookup(manglingName) is called unconditionally, but nameLookup is copied verbatim from the exported Config.NameLookup field (compile.go:134), which has no default and is not validated in NewPackage/loadFile. The doc comment (compile.go:83-85) doesn't state the field is required. Any caller constructing a Config without setting NameLookup will hit a nil-function call and panic on the first FunctionDecl. Only the test mock sets it today. Suggest either guarding (if ctx.nameLookup == nil { ... }, treating nil as "always found" or skip), validating in NewPackage with a clear error, or at minimum documenting that NameLookup is mandatory.

if debugCompileDecl {
log.Println("func", clang.String(fn), "- skipped")
}
return
}

fnName := clang.String(fn)
if debugCompileDecl {
log.Println("func", fnName, "-", clang.String(fn.Type()))
Expand Down Expand Up @@ -234,93 +201,16 @@ func compileFunc(ctx *blockCtx, fn clang.Cursor) {
if err != nil {
log.Panicln("compileFunc:", fnName, err)
}
// ctx.addExternFunc(fnName)
ctx.forceImportUnsafe()
f.SetComments(pkg, &ast.CommentGroup{
List: []*ast.Comment{
{Text: "\n//go:linkname " + fnName + " C." + manglingName[1:]},

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.

[P1] manglingName[1:] assumes a leading underscore (not portable to ELF)

"C." + manglingName[1:] unconditionally strips the first byte of the mangling name. This is correct only where clang prepends a leading underscore (Darwin/Mach-O, e.g. _g -> mangling __g -> C._g, matching the expected out.go). On Linux/ELF, C symbols typically have no leading underscore, so [1:] drops a real, significant character (e.g. xprintf -> printf), producing an incorrect linkname target. It also silently truncates single-char symbols. Suggest stripping the underscore conditionally (strings.TrimPrefix / check the prefix) or gating on the target object format, and guarding the empty-string case. A short comment explaining the convention would also prevent a future regression.

},
})
if rewritten {
scope := pkg.Types.Scope()
substObj(pkg.Types, scope, origName, f)
}
/* origName, rewritten := fnName, false
if !ctx.inHeader && fn.StorageClass == ast.Static {
fnName, rewritten = ctx.autoStaticName(origName), true
} else {
rewritten = ctx.getPubName(&fnName)
}
if body != nil {
if ctx.checkExists(fnName) {
return
}
isMain := false
if fnName == "main" && (results != nil || params != nil) {
fnName, isMain = "_cgo_main", true
}
f, err := pkg.NewFuncWith(ctx.goNodePos(fn), fnName, sig, nil)
if err != nil {
log.Panicln("compileFunc:", err)
}
if rewritten { // for fnName is a recursive function
scope := pkg.Types.Scope()
substObj(pkg.Types, scope, origName, f.Obj())
rewritten = false
}
cb := f.BodyStart(pkg)
ctx.curfn = newFuncCtx(pkg, ctx.markComplicated(fnName, body), origName)
compileSub(ctx, body)
checkNeedReturn(ctx, body)
ctx.curfn = nil
cb.End()
if isMain {
var t *types.Var
var entryParams *types.Tuple
var entry = "main"
var testMain = ctx.testMain
if testMain {
entry = "TestMain"
testing := pkg.Import("testing")
t = pkg.NewParam(token.NoPos, "t", types.NewPointer(testing.Ref("T").Type()))
entryParams = types.NewTuple(t)
}
pkg.NewFunc(nil, entry, entryParams, nil, false).BodyStart(pkg)
if results != nil {
if testMain {
// if _cgo_ret := _cgo_main(); _cgo_ret != 0 {
// t.Fatal("exit status", _cgo_ret)
// }
cb.If().DefineVarStart(token.NoPos, retName)
} else {
// os.Exit(int(_cgo_main()))
cb.Val(pkg.Import("os").Ref("Exit")).Typ(types.Typ[types.Int])
}
}
cb.Val(f.Obj())
if params != nil {
panic("TODO: main func with params")
}
cb.Call(len(params))
if results != nil {
if testMain {
cb.EndInit(1)
ret := cb.Scope().Lookup(retName)
cb.Val(ret).Val(0).BinaryOp(token.NEQ).Then().
Val(t).MemberVal("Fatal").Val("exit status").Val(ret).Call(2).EndStmt().
End()
} else {
cb.Call(1).Call(1)
}
}
cb.EndStmt().End()
} else {
delete(ctx.extfns, fnName)
}
} else if fn.IsUsed {
f := types.NewFunc(ctx.goNodePos(fn), pkg.Types, fnName, sig)
if pkg.Types.Scope().Insert(f) == nil {
ctx.addExternFunc(fnName)
}
}
if rewritten {
scope := pkg.Types.Scope()
substObj(pkg.Types, scope, origName, scope.Lookup(fnName))
} */
}

var (
Expand All @@ -341,7 +231,7 @@ func newParam(ctx *blockCtx, decl clang.Cursor, i c.Int) *types.Var {
if declName != "" {
avoidKeyword(&declName)
} else {
declName = "__llcppg_param" + strconv.Itoa(int(i)+1)
declName = "_llcppg_param" + strconv.Itoa(int(i)+1)
}
return types.NewParam(goNodePos(ctx, decl), ctx.pkg.Types, declName, typ)
}
Expand Down
37 changes: 37 additions & 0 deletions clang/mangling_addprefix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build linux

/*
* 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 clang

import (
"github.com/goplus/lib/c"
)

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

/**
* Retrieve a name for the entity referenced by this cursor.
*/
func Mangling(fn Cursor) string {
m := fn.Mangling()
manglingName := c.GoString(m.CStr())
m.Dispose()
return "_" + manglingName
}

// -----------------------------------------------------------------------------
Loading
Loading