diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index bdf14f41c6..ff96589aa0 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -818,7 +818,8 @@ runs: # LLVM/LLD toolchain above on PATH for WASI and other cross-target links. # Release-artifact jobs also need it when install-llvm=false. if [[ "$RUNNER_ARCH" == ARM64 ]]; then - LLGO_INSTALLER_LIBRARY_ONLY=1 source ./install.sh + # Consumers may check out LLGo below their workspace root. + LLGO_INSTALLER_LIBRARY_ONLY=1 source "$GITHUB_ACTION_PATH/../../../install.sh" install_macos_lld fi diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a0d308321d..4896fcd25f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -39,7 +39,10 @@ jobs: # Individual Go test packages retain their existing timeout limits. # Native PCLN integration can relink each fixture; retain room for the # serial coverage suites and their upload on a cold Linux cache. - timeout-minutes: 90 + # Windows MSVC has reached 89 minutes after successful tests and coverage + # upload, then hit the 90-minute limit while saving the LLVM archive cache. + # Give that lane room for cold dependency setup and post-job cache saves. + timeout-minutes: ${{ matrix.windows_abi == 'msvc' && 120 || 90 }} strategy: fail-fast: false matrix: diff --git a/_demo/c/go.mod b/_demo/c/go.mod index 73d9b592c7..6c7ee7adae 100644 --- a/_demo/c/go.mod +++ b/_demo/c/go.mod @@ -1,5 +1,8 @@ module github.com/xgo-dev/llgo/_demo/c -go 1.20 +go 1.23 require github.com/goplus/lib v0.5.1 + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/_demo/c/go.sum b/_demo/c/go.sum index 389ae9c56b..3c31f27fbf 100644 --- a/_demo/c/go.sum +++ b/_demo/c/go.sum @@ -1,2 +1,2 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= diff --git a/_demo/embed/esp32/hello/uart.go b/_demo/embed/esp32/hello/uart.go index 1cd3a5aba9..d7e54a554c 100644 --- a/_demo/embed/esp32/hello/uart.go +++ b/_demo/embed/esp32/hello/uart.go @@ -9,5 +9,5 @@ import ( //go:linkname WriteByte C.board_uart_write_char func WriteByte(b byte) -//go:linkname sleep sleep +//go:linkname sleep C.sleep func sleep(c c.Int) diff --git a/_demo/embed/esp32/rt/main.go b/_demo/embed/esp32/rt/main.go index eb78c59e67..4535e12fe8 100644 --- a/_demo/embed/esp32/rt/main.go +++ b/_demo/embed/esp32/rt/main.go @@ -7,103 +7,103 @@ import ( ) // -//go:linkname absvdi2 __absvdi2 +//go:linkname absvdi2 C.__absvdi2 func absvdi2(a int64) int64 -//go:linkname absvsi2 __absvsi2 +//go:linkname absvsi2 C.__absvsi2 func absvsi2(a int32) int32 -//go:linkname adddf3 __adddf3 +//go:linkname adddf3 C.__adddf3 func adddf3(a, b float64) float64 -//go:linkname addsf3 __addsf3 +//go:linkname addsf3 C.__addsf3 func addsf3(a, b float32) float32 -//go:linkname addvdi3 __addvdi3 +//go:linkname addvdi3 C.__addvdi3 func addvdi3(a, b int64) int64 -//go:linkname addvsi3 __addvsi3 +//go:linkname addvsi3 C.__addvsi3 func addvsi3(a, b int32) int32 -//go:linkname udivdi3 __udivdi3 +//go:linkname udivdi3 C.__udivdi3 func udivdi3(a, b uint64) uint64 -//go:linkname clzdi2 __clzdi2 +//go:linkname clzdi2 C.__clzdi2 func clzdi2(a uint64) int32 -//go:linkname clzsi2 __clzsi2 +//go:linkname clzsi2 C.__clzsi2 func clzsi2(a uint32) int32 -//go:linkname ctzdi2 __ctzdi2 +//go:linkname ctzdi2 C.__ctzdi2 func ctzdi2(a uint64) int32 -//go:linkname ctzsi2 __ctzsi2 +//go:linkname ctzsi2 C.__ctzsi2 func ctzsi2(a uint32) int32 -//go:linkname popcountdi2 __popcountdi2 +//go:linkname popcountdi2 C.__popcountdi2 func popcountdi2(a uint64) int32 -//go:linkname popcountsi2 __popcountsi2 +//go:linkname popcountsi2 C.__popcountsi2 func popcountsi2(a uint32) int32 -//go:linkname divdf3 __divdf3 +//go:linkname divdf3 C.__divdf3 func divdf3(a, b float64) float64 -//go:linkname divsf3 __divsf3 +//go:linkname divsf3 C.__divsf3 func divsf3(a, b float32) float32 -//go:linkname mulsf3 __mulsf3 +//go:linkname mulsf3 C.__mulsf3 func mulsf3(a, b float32) float32 -//go:linkname divdi3 __divdi3 +//go:linkname divdi3 C.__divdi3 func divdi3(a, b int64) int64 -//go:linkname muldf3 __muldf3 +//go:linkname muldf3 C.__muldf3 func muldf3(a, b float64) float64 -//go:linkname muldi3 __muldi3 +//go:linkname muldi3 C.__muldi3 func muldi3(a, b int64) int64 -//go:linkname subdf3 __subdf3 +//go:linkname subdf3 C.__subdf3 func subdf3(a, b float64) float64 -//go:linkname subsf3 __subsf3 +//go:linkname subsf3 C.__subsf3 func subsf3(a, b float32) float32 -//go:linkname extendsfdf2 __extendsfdf2 +//go:linkname extendsfdf2 C.__extendsfdf2 func extendsfdf2(a float32) float64 -//go:linkname fixdfdi __fixdfdi +//go:linkname fixdfdi C.__fixdfdi func fixdfdi(a float64) int64 -//go:linkname fixdfsi __fixdfsi +//go:linkname fixdfsi C.__fixdfsi func fixdfsi(a float64) int32 -//go:linkname fixsfdi __fixsfdi +//go:linkname fixsfdi C.__fixsfdi func fixsfdi(a float32) int64 -//go:linkname fixsfsi __fixsfsi +//go:linkname fixsfsi C.__fixsfsi func fixsfsi(a float32) int32 -//go:linkname floatdidf __floatdidf +//go:linkname floatdidf C.__floatdidf func floatdidf(a int64) float64 -//go:linkname floatsidf __floatsidf +//go:linkname floatsidf C.__floatsidf func floatsidf(a int32) float64 -//go:linkname ashldi3 __ashldi3 +//go:linkname ashldi3 C.__ashldi3 func ashldi3(a int64, b int32) int64 -//go:linkname ashrdi3 __ashrdi3 +//go:linkname ashrdi3 C.__ashrdi3 func ashrdi3(a int64, b int32) int64 -//go:linkname lshrdi3 __lshrdi3 +//go:linkname lshrdi3 C.__lshrdi3 func lshrdi3(a uint64, b int32) uint64 -//go:linkname bswapdi2 __bswapdi2 +//go:linkname bswapdi2 C.__bswapdi2 func bswapdi2(a uint64) uint64 -//go:linkname bswapsi2 __bswapsi2 +//go:linkname bswapsi2 C.__bswapsi2 func bswapsi2(a uint32) uint32 var totalTests = 0 diff --git a/_demo/embed/go.mod b/_demo/embed/go.mod index a51c6301e0..e6ea6cf3d8 100644 --- a/_demo/embed/go.mod +++ b/_demo/embed/go.mod @@ -1,5 +1,8 @@ module github.com/xgo-dev/llgo/_demo/embed -go 1.20 +go 1.23 require github.com/goplus/lib v0.5.1 + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/_demo/embed/go.sum b/_demo/embed/go.sum index 389ae9c56b..3c31f27fbf 100644 --- a/_demo/embed/go.sum +++ b/_demo/embed/go.sum @@ -1,2 +1,2 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= diff --git a/_demo/go/go.mod b/_demo/go/go.mod index f5937cbd0b..82bdeaa31e 100644 --- a/_demo/go/go.mod +++ b/_demo/go/go.mod @@ -1,5 +1,8 @@ module github.com/xgo-dev/llgo/_demo/go -go 1.20 +go 1.23 require github.com/goplus/lib v0.5.1 + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/_demo/go/go.sum b/_demo/go/go.sum index 389ae9c56b..3c31f27fbf 100644 --- a/_demo/go/go.sum +++ b/_demo/go/go.sum @@ -1,2 +1,2 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= diff --git a/_demo/py/go.mod b/_demo/py/go.mod index 9ffbdfc637..87519de420 100644 --- a/_demo/py/go.mod +++ b/_demo/py/go.mod @@ -1,5 +1,8 @@ module github.com/xgo-dev/llgo/_demo/py -go 1.20 +go 1.23 require github.com/goplus/lib v0.5.1 + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/_demo/py/go.sum b/_demo/py/go.sum index 389ae9c56b..3c31f27fbf 100644 --- a/_demo/py/go.sum +++ b/_demo/py/go.sum @@ -1,2 +1,2 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= diff --git a/_xtool/castdump/castdump.go b/_xtool/castdump/castdump.go index ee771bcdbc..543f989d59 100644 --- a/_xtool/castdump/castdump.go +++ b/_xtool/castdump/castdump.go @@ -7,12 +7,12 @@ import ( "unsafe" "github.com/goplus/lib/c" - "github.com/goplus/lib/c/clang" + "github.com/llarhub/clang-c" ) type Data struct { Depth c.Uint - Unit *clang.TranslationUnit + Unit clang.TranslationUnit } var accessMap = map[clang.CXXAccessSpecifier]string{ @@ -42,32 +42,32 @@ func visit(cursor, parent clang.Cursor, ClientData c.Pointer) clang.ChildVisitRe func printType(t clang.Type, data *Data) { printIndent(data.Depth) - typeSpell := t.String() - typeKind := t.Kind.String() + typeSpell := t.Spelling() + typeKind := t.Kind.Spelling() - if t.Kind == clang.TypeInvalid { - } else if t.Kind == clang.TypeUnexposed { + if t.Kind == clang.Type_Invalid { + } else if t.Kind == clang.Type_Unexposed { c.Printf(c.Str(": %s\n"), typeKind.CStr(), typeSpell.CStr()) - } else if t.Kind >= clang.TypeFirstBuiltin && t.Kind <= clang.TypeLastBuiltin { + } else if t.Kind >= clang.Type_FirstBuiltin && t.Kind <= clang.Type_LastBuiltin { c.Printf(c.Str(": %s\n"), typeKind.CStr(), typeSpell.CStr()) - } else if t.Kind > clang.TypeComplex { + } else if t.Kind > clang.Type_Complex { c.Printf(c.Str(": %s\n"), typeKind.CStr(), typeSpell.CStr()) } data.Depth++ switch t.Kind { - case clang.TypePointer: - printType(t.PointeeType(), data) - case clang.TypeIncompleteArray, clang.TypeVariableArray, clang.TypeDependentSizedArray, clang.TypeConstantArray: - printType(t.ArrayElementType(), data) - case clang.TypeTypedef: - printType(t.TypeDeclaration().TypedefDeclUnderlyingType(), data) - case clang.TypeElaborated: - printType(t.NamedType(), data) - case clang.TypeFunctionProto: - printType(t.ResultType(), data) + case clang.Type_Pointer: + printType(t.Pointee(), data) + case clang.Type_IncompleteArray, clang.Type_VariableArray, clang.Type_DependentSizedArray, clang.Type_ConstantArray: + printType(t.ArrayElement(), data) + case clang.Type_Typedef: + printType(t.Declaration().TypedefDeclUnderlyingType(), data) + case clang.Type_Elaborated: + printType(t.Named(), data) + case clang.Type_FunctionProto: + printType(t.Result(), data) for i := 0; i < int(t.NumArgTypes()); i++ { - printType(t.ArgType(c.Uint(i)), data) + printType(t.Arg(c.Uint(i)), data) } } data.Depth-- @@ -81,16 +81,16 @@ func printLocation(cursor clang.Cursor) { var file clang.File var line, column c.Uint - loc.SpellingLocation(&file, &line, &column, nil) - filename := file.FileName() + loc.Spelling(&file, &line, &column, nil) + filename := file.Name() defer filename.Dispose() c.Printf(c.Str("(Loc:%s:%d:%d)\n"), filename.CStr(), line, column) } func printAccess(cursor clang.Cursor) { - kind := cursor.Kind.String() - spell := cursor.String() + kind := cursor.Kind.Spelling() + spell := cursor.Spelling() defer kind.Dispose() defer spell.Dispose() @@ -98,8 +98,8 @@ func printAccess(cursor clang.Cursor) { printLocation(cursor) } -func printMacro(cursor clang.Cursor, unit *clang.TranslationUnit) { - kind := cursor.Kind.String() +func printMacro(cursor clang.Cursor, unit clang.TranslationUnit) { + kind := cursor.Kind.Spelling() defer kind.Dispose() c.Printf(c.Str("%s: "), kind.CStr()) @@ -111,7 +111,7 @@ func printMacro(cursor clang.Cursor, unit *clang.TranslationUnit) { tokensSlice := unsafe.Slice(tokens, int(numTokens)) for _, tok := range tokensSlice { - tokStr := unit.Token(tok) + tokStr := unit.TokenSpelling(tok) c.Printf(c.Str("%s "), tokStr.CStr()) tokStr.Dispose() } @@ -119,8 +119,8 @@ func printMacro(cursor clang.Cursor, unit *clang.TranslationUnit) { } func printFunc(cursor clang.Cursor, data *Data) { - kind := cursor.Kind.String() - spell := cursor.String() + kind := cursor.Kind.Spelling() + spell := cursor.Spelling() symbol := cursor.Mangling() defer symbol.Dispose() defer kind.Dispose() @@ -132,8 +132,8 @@ func printFunc(cursor clang.Cursor, data *Data) { } func printEnumConstant(cursor clang.Cursor) { - kind := cursor.Kind.String() - spell := cursor.String() + kind := cursor.Kind.Spelling() + spell := cursor.Spelling() defer kind.Dispose() defer spell.Dispose() @@ -142,13 +142,13 @@ func printEnumConstant(cursor clang.Cursor) { } func printDefault(cursor clang.Cursor, data *Data) { - kind := cursor.Kind.String() - spell := cursor.String() + kind := cursor.Kind.Spelling() + spell := cursor.Spelling() defer kind.Dispose() defer spell.Dispose() // node which has type - if cursor.Type().Kind != clang.TypeInvalid { + if cursor.Type().Kind != clang.Type_Invalid { c.Printf(c.Str("%s: %s"), kind.CStr(), spell.CStr()) printLocation(cursor) printType(cursor.Type(), data) @@ -158,19 +158,19 @@ func printDefault(cursor clang.Cursor, data *Data) { } func printAST(cursor clang.Cursor, data *Data) { - kind := cursor.Kind.String() - spell := cursor.String() + kind := cursor.Kind.Spelling() + spell := cursor.Spelling() printIndent(data.Depth) switch cursor.Kind { - case clang.CursorCXXAccessSpecifier: + case clang.Cursor_CXXAccessSpecifier: printAccess(cursor) - case clang.CursorMacroDefinition: + case clang.Cursor_MacroDefinition: printMacro(cursor, data.Unit) - case clang.CursorFunctionDecl, clang.CursorCXXMethod, clang.CursorConstructor, clang.CursorDestructor: + case clang.Cursor_FunctionDecl, clang.Cursor_CXXMethod, clang.Cursor_Constructor, clang.Cursor_Destructor: printFunc(cursor, data) - case clang.CursorEnumConstantDecl: + case clang.Cursor_EnumConstantDecl: printEnumConstant(cursor) default: printDefault(cursor, data) @@ -201,7 +201,7 @@ func main() { sourceFile, unsafe.SliceData(args), 3, nil, 0, - clang.DetailedPreprocessingRecord, + c.Uint(clang.TranslationUnit_DetailedPreprocessingRecord), ) defer index.Dispose() defer unit.Dispose() diff --git a/_xtool/go.mod b/_xtool/go.mod index d01bdf498e..e0490f2436 100644 --- a/_xtool/go.mod +++ b/_xtool/go.mod @@ -1,5 +1,11 @@ module github.com/xgo-dev/llgo/_xtool -go 1.20 +go 1.23 -require github.com/goplus/lib v0.5.1 +require ( + github.com/goplus/lib v0.5.3 + github.com/llarhub/clang-c v0.6.1 +) + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/_xtool/go.sum b/_xtool/go.sum index 389ae9c56b..636dbbe2e6 100644 --- a/_xtool/go.sum +++ b/_xtool/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= +github.com/llarhub/clang-c v0.6.1 h1:EHb4qXvH5hwEZbsWpub0v7lID4wo5LmyS/KEp8U54FU= +github.com/llarhub/clang-c v0.6.1/go.mod h1:sZhzXACY6REPetdPrGWcSvRmVkl9RgMHbGWHGeOTchI= diff --git a/cl/_testlibc/argv/in.go b/cl/_testlibc/argv/in.go index cc79657bca..91ed4dd027 100644 --- a/cl/_testlibc/argv/in.go +++ b/cl/_testlibc/argv/in.go @@ -6,10 +6,10 @@ import _ "unsafe" type integer interface{ ~int32 } -//go:linkname argc __llgo_argc +//go:linkname argc C.__llgo_argc var argc int32 -//go:linkname argv __llgo_argv +//go:linkname argv C.__llgo_argv var argv **int8 // llgo:link index llgo.index diff --git a/cl/_testlibc/once/local/once.go b/cl/_testlibc/once/local/once.go index 4005750c53..e681463c2c 100644 --- a/cl/_testlibc/once/local/once.go +++ b/cl/_testlibc/once/local/once.go @@ -11,7 +11,7 @@ const ( type Once C.pthread_once_t -//go:linkname OnceInit llgoSyncOnceInitVal +//go:linkname OnceInit C.llgoSyncOnceInitVal var OnceInit Once // llgo:link (*Once).Do C.pthread_once diff --git a/cl/_testlibc/setjmp/stderr_darwin.go b/cl/_testlibc/setjmp/stderr_darwin.go index 955d154663..9d1e01c33b 100644 --- a/cl/_testlibc/setjmp/stderr_darwin.go +++ b/cl/_testlibc/setjmp/stderr_darwin.go @@ -4,5 +4,5 @@ package main import "unsafe" -//go:linkname stderr __stderrp +//go:linkname stderr C.__stderrp var stderr unsafe.Pointer diff --git a/cl/_testlibc/setjmp/stderr_unix.go b/cl/_testlibc/setjmp/stderr_unix.go index 2f6e8012f5..cf40223560 100644 --- a/cl/_testlibc/setjmp/stderr_unix.go +++ b/cl/_testlibc/setjmp/stderr_unix.go @@ -4,5 +4,5 @@ package main import "unsafe" -//go:linkname stderr stderr +//go:linkname stderr C.stderr var stderr unsafe.Pointer diff --git a/cl/_testrt/cvar/in.go b/cl/_testrt/cvar/in.go index 75db1c72b2..0e89e9ec0b 100644 --- a/cl/_testrt/cvar/in.go +++ b/cl/_testrt/cvar/in.go @@ -1,11 +1,12 @@ -// LITTEST +// LITTEST darwin/arm64 linux/amd64 windows/386 windows/amd64 windows/arm64 +// Scope: common (native global storage, including function-pointer fields) package main import _ "unsafe" // CHECK: {{^}}@_bar_x = external global { [16 x i8], [2 x ptr] } // -//go:linkname barX _bar_x +//go:linkname barX C._bar_x var barX struct { Arr [16]int8 Callbacks [2]func() @@ -13,7 +14,7 @@ var barX struct { // CHECK: {{^}}@_bar_y = external global { [16 x i8] } // -//go:linkname barY _bar_y +//go:linkname barY C._bar_y var barY struct { Arr [16]int8 } diff --git a/cl/compile.go b/cl/compile.go index 4fe3617c5a..e245801dc0 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -414,23 +414,37 @@ func (p *context) compileMethodsIf(pkg llssa.Package, typ types.Type, keep func( } // Global variable. -func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { +func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) error { typ := p.patchType(gbl.Type()) name, vtype, define := p.varName(gbl.Pkg.Pkg, gbl) if vtype == pyVar { - return + return nil } dbgInstrln("==> NewVar", name, typ) - g, skip := p.localityGlobalStorage(pkg, gbl, name, typ, llssa.CallConv(vtype)) + g, skip, info := p.localityGlobalStorage(pkg, gbl, name, typ, llssa.CallConv(vtype)) if skip { - return + return nil } if define && name == llssa.RuntimeGoroutineStackSizeVar { p.prog.InitPthreadStackSize(g) - return + return nil } if p.tryEmbedGlobalInit(pkg, gbl, g, name) { - return + return nil + } + if define && vtype == goVar && info.Locality == llssa.LocalityNone { + init := p.staticGlobalInits[gbl] + if value, ok := p.rewriteValue(name); ok { + if p.isStringPtrType(gbl.Type()) { + init = pkg.ConstString(value) + } else { + log.Printf("warning: ignoring rewrite for non-string variable %s (type: %v)", name, gbl.Type()) + } + } + if err := g.InitSharedGo(init, p.prog.IsExternalImport(name)); err != nil { + return fmt.Errorf("%s: %s: %w", p.fset.Position(gbl.Pos()), gbl.Name(), err) + } + return nil } if value, ok := p.rewriteValue(name); ok { if p.isStringPtrType(gbl.Type()) { @@ -446,6 +460,7 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { } else if define { g.InitNil() } + return nil } func (p *context) makeClosureCtx(fn *ssa.Function, pkg *types.Package, vars []*ssa.FreeVar) *types.Var { @@ -2985,12 +3000,16 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri if _, ok := skips["init"]; ok || ctx.skipall { ctx.state |= pkgFNoOldInit } - processPkg(ctx, ret, patch.Alt) + if err = processPkg(ctx, ret, patch.Alt); err != nil { + return nil, nil, err + } ctx.state = pkgHasPatch ctx.skips = skips } if !ctx.skipall { - processPkg(ctx, ret, pkg) + if err = processPkg(ctx, ret, pkg); err != nil { + return nil, nil, err + } } for len(ctx.inits) > 0 { inits := ctx.inits @@ -3017,7 +3036,7 @@ func initFnNameOfHasPatch(name string) string { return name + "$hasPatch" } -func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { +func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) error { type namedMember struct { name string val ssa.Member @@ -3065,10 +3084,13 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { ctx.compileType(ret, member) case *ssa.Global: if !isCgoFuncPtrVar(member.Name()) { - ctx.compileGlobal(ret, member) + if err := ctx.compileGlobal(ret, member); err != nil { + return err + } } } } + return nil } func (p *context) type_(typ types.Type, cc llssa.CallConv) llssa.Type { diff --git a/cl/compile_test.go b/cl/compile_test.go index cce31fbc77..082638adfa 100644 --- a/cl/compile_test.go +++ b/cl/compile_test.go @@ -40,6 +40,9 @@ import ( func testCompile(t *testing.T, src, expected string) { t.Helper() + if runtime.GOOS == "darwin" { + expected = strings.ReplaceAll(expected, "common global", "weak global") + } if runtime.GOOS == "windows" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") { // Win64 definitions need unwind tables even when LLVM infers nounwind. expected = strings.ReplaceAll(expected, "null_pointer_is_valid", "null_pointer_is_valid uwtable") @@ -965,8 +968,8 @@ var a int `, `; ModuleID = 'foo' source_filename = "foo" -@foo.a = global i64 0, align 8 -@"foo.init$guard" = global i1 false, align 1 +@foo.a = common global i64 0, align 8 +@"foo.init$guard" = common global i1 false, align 1 ; Function Attrs: null_pointer_is_valid define void @foo.init() #0 { @@ -995,7 +998,7 @@ func fn(a int, b float64) int { `, `; ModuleID = 'foo' source_filename = "foo" -@"foo.init$guard" = global i1 false, align 1 +@"foo.init$guard" = common global i1 false, align 1 ; Function Attrs: null_pointer_is_valid define i64 @foo.fn(i64 %0, double %1) #0 { @@ -1035,7 +1038,7 @@ func use(b bool) uint8 { `, `; ModuleID = 'foo' source_filename = "foo" -@"foo.init$guard" = global i1 false, align 1 +@"foo.init$guard" = common global i1 false, align 1 ; Function Attrs: null_pointer_is_valid define void @foo.init() #0 { diff --git a/cl/import.go b/cl/import.go index d80ab3ae6d..3f79a55fc0 100644 --- a/cl/import.go +++ b/cl/import.go @@ -936,22 +936,22 @@ func (p *context) varName(pkg *types.Package, v *ssa.Global) (vName string, vtyp // TODO(lijie): need a bettery way to process linkname (maybe alias) if !isCgoCfpvar(v.Name()) && !isCgoVar(v.Name()) { if v, ok := p.prog.Linkname(name); ok { + if strings.HasPrefix(v, "C.") { + return v[2:], cVar, false + } if strings.HasPrefix(v, "stdcall.") { panic(fmt.Errorf("stdcall linkname namespace applies only to functions: %s", name)) } if strings.HasPrefix(v, "go:") { - if llssa.PathOf(pkg) == "runtime" { - return v, goVar, true - } - return v, goVar, false + return v, goVar, true } if pos := strings.IndexByte(v, '.'); pos >= 0 { if pos == 2 && v[0] == 'p' && v[1] == 'y' { return v[3:], pyVar, false } - return replaceGoName(v, pos), goVar, false + return replaceGoName(v, pos), goVar, true } - return v, cVar, false + return v, goVar, true } } return name, goVar, true @@ -1095,6 +1095,21 @@ func ParsePkgSyntaxWithOptions(prog llssa.Program, fset *token.FileSet, pkg *typ if err := collectPackageLinknames(prog, fset, files, syms, linkDocs); err != nil { return err } + for _, file := range files { + for _, group := range file.Comments { + for _, comment := range group.List { + // Only canonical gc line directives affect Go storage here. + // The build linker-options parser also accepts legacy spellings; + // those must not silently turn a Go definition into an extern. + if strings.HasPrefix(comment.Text, "//go:cgo_import_dynamic ") || strings.HasPrefix(comment.Text, "//go:cgo_import_static ") { + fields := strings.Fields(comment.Text) + if len(fields) >= 2 && fields[1] != "_" { + prog.SetExternalImport(fields[1]) + } + } + } + } + } prog.MarkPackageSyntaxParsed(pkg) return nil } diff --git a/cl/import_patch_test.go b/cl/import_patch_test.go index ffb70e358f..d8380429b4 100644 --- a/cl/import_patch_test.go +++ b/cl/import_patch_test.go @@ -65,7 +65,7 @@ func TestVarNameGoLinkRuntimeAndNonRuntime(t *testing.T) { key := llssa.FullName(pkg, global.Name()) prog.SetLinkname(key, "go:example.com/p.G") name, vtyp, define = c.varName(pkg, global) - if name != "go:example.com/p.G" || vtyp != goVar || define { - t.Fatalf("non-runtime varName = (%q,%d,%v), want (%q,%d,%v)", name, vtyp, define, "go:example.com/p.G", goVar, false) + if name != "go:example.com/p.G" || vtyp != goVar || !define { + t.Fatalf("non-runtime varName = (%q,%d,%v), want (%q,%d,%v)", name, vtyp, define, "go:example.com/p.G", goVar, true) } } diff --git a/cl/linkname_storage_test.go b/cl/linkname_storage_test.go new file mode 100644 index 0000000000..ff2cae6ba1 --- /dev/null +++ b/cl/linkname_storage_test.go @@ -0,0 +1,172 @@ +package cl + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + llssa "github.com/xgo-dev/llgo/ssa" + "github.com/xgo-dev/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestLinknameVariableStorageIR(t *testing.T) { + const source = `package shared +import _ "unsafe" +//go:linkname Zero shared_zero +var Zero int32 +//go:linkname Static shared_static +var Static int32 = 41 +//go:linkname Callback shared_callback +var Callback func(int32) int32 +//go:linkname Native C.shared_native +var Native func(int32) int32 +//go:linkname Alias shared_static +var Alias int32 +//go:linkname Reserved go:shared_value +var Reserved int32 +//go:cgo_import_dynamic shared_import imported "libexample.so" +//go:linkname Imported shared_import +var Imported uintptr +//go:cgo_import_dynamic shared.Default imported "libexample.so" +var Default uintptr +//go:cgo_import_static static_import +//go:linkname StaticImport static_import +var StaticImport byte +//go:cgo_import_dynamic shared.Initialized imported "libexample.so" +var Initialized uintptr = 7 +` + for _, target := range []*llssa.Target{ + {GOOS: "darwin", GOARCH: "arm64"}, {GOOS: "linux", GOARCH: "amd64"}, + {GOOS: "windows", GOARCH: "386"}, {GOOS: "windows", GOARCH: "amd64"}, + {GOOS: "windows", GOARCH: "arm64"}, {GOOS: "wasip1", GOARCH: "wasm"}, + } { + t.Run(target.GOOS+"/"+target.GOARCH, func(t *testing.T) { + ir := compileWithRewritesTarget(t, source, nil, target) + for _, want := range []string{ + "@shared_zero = common global i32 0", "@shared_static = global i32 41", + "@shared_callback = common global { ptr, ptr } zeroinitializer", + "@shared_native = external global ptr", `@"go:shared_value" = common global i32 0`, + "@static_import = external global i8", "@shared_import = external global i", "@shared.Default = external global i", + fmt.Sprintf("@shared.Initialized = global i%d 7", map[bool]int{true: 32, false: 64}[target.GOARCH == "386" || target.GOARCH == "wasm"]), + } { + if target.GOARCH == "wasm" || target.GOOS == "darwin" { + want = strings.ReplaceAll(want, "common global", "weak global") + } + if !strings.Contains(ir, want) { + t.Errorf("missing %q:\n%s", want, ir) + } + } + }) + } +} + +func TestLinknameDuplicateStaticDiagnostic(t *testing.T) { + const source = `package duplicate +import _ "unsafe" +//go:linkname A shared_static +var A int32 = 41 +//go:linkname B shared_static +var B int32 = 42 +` + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "duplicate.go", source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + imp := importer.Default() + files := []*ast.File{file} + pkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, types.NewPackage("duplicate", "duplicate"), files, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgramEx(t, nil, imp) + _, _, err = NewPackageEx(prog, nil, nil, pkg, files) + if err == nil { + t.Fatal("duplicate definitions compiled") + } + for _, want := range []string{"duplicate.go:6:", "B:", "duplicate definition", "shared_static"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("diagnostic %q is missing %q", err, want) + } + } +} + +func TestLinknameExternalImportAcrossPackages(t *testing.T) { + fset := token.NewFileSet() + imp := importer.Default() + prog := ssatest.NewProgramEx(t, nil, imp) + imports, err := parser.ParseFile(fset, "imports.go", `package imports +//go:cgo_import_dynamic shared_import remote "libexample.so" +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + if err := ParsePkgSyntax(prog, fset, types.NewPackage("imports", "imports"), []*ast.File{imports}); err != nil { + t.Fatal(err) + } + consumer, err := parser.ParseFile(fset, "consumer.go", `package consumer +import _ "unsafe" +//go:linkname Value shared_import +var Value uintptr +var Unrelated uintptr +`, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{consumer} + pkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, types.NewPackage("consumer", "consumer"), files, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + compiled, _, err := NewPackageEx(prog, nil, nil, pkg, files) + if err != nil { + t.Fatal(err) + } + mod := compiled.Module() + if !mod.NamedGlobal("shared_import").Initializer().IsNil() { + t.Fatal("import in another package did not replace eligible zero storage") + } + if mod.NamedGlobal("consumer.Unrelated").Initializer().IsNil() { + t.Fatal("unrelated Go variable lost storage") + } +} + +func TestLinknameImportCanonicalSpelling(t *testing.T) { + for _, tc := range []struct { + directive string + imported bool + }{ + {"//go:cgo_import_dynamic shared remote \"libexample.so\"", true}, + {"//go:cgo_import_static shared", true}, + {"// go:cgo_import_static shared", false}, + {"/*go:cgo_import_static shared*/", false}, + {"//go:cgo_import_staticX shared", false}, + {"//go:cgo_import_static\tshared", false}, + {"//go:cgo_import_static _", false}, + } { + t.Run(tc.directive, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "imports.go", "package imports\n"+tc.directive+"\n", parser.ParseComments) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgramEx(t, nil, importer.Default()) + if err := ParsePkgSyntax(prog, fset, types.NewPackage("imports", "imports"), []*ast.File{file}); err != nil { + t.Fatal(err) + } + if got := prog.IsExternalImport("shared"); got != tc.imported { + t.Fatalf("IsExternalImport = %v, want %v", got, tc.imported) + } + if prog.IsExternalImport("_") { + t.Fatal("library-only pragma created a variable import") + } + }) + } +} diff --git a/cl/locality_lower.go b/cl/locality_lower.go index cda66b8575..a176536815 100644 --- a/cl/locality_lower.go +++ b/cl/locality_lower.go @@ -173,22 +173,22 @@ func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, define return &localVariable{planned: planned, owner: owner}, true, nil } -func (p *context) localityGlobalStorage(pkg llssa.Package, global *ssa.Global, name string, typ types.Type, cc llssa.CallConv) (llssa.Global, bool) { +func (p *context) localityGlobalStorage(pkg llssa.Package, global *ssa.Global, name string, typ types.Type, cc llssa.CallConv) (llssa.Global, bool, llssa.VariableLocality) { info, ok := p.resolveLocality(global.Pkg.Pkg, llssa.FullName(global.Pkg.Pkg, global.Name())) if !ok || info.Locality == locality.None { - return pkg.NewVar(name, typ, cc), false + return pkg.NewVar(name, typ, cc), false, info } variable := p.locality.variables[global] if variable == nil { panic(fmt.Sprintf("missing locality layout for %s", name)) } if p.prog.LogicalGoroutineLocalityEnabled() && variable.planned.Info.Locality == locality.Goroutine { - return nil, true + return nil, true, info } if variable.planned.Storage == localitylayout.StoragePackage { - return nil, true + return nil, true, info } - return variable.owner.direct[variable.planned.Name], false + return variable.owner.direct[variable.planned.Name], false, info } func (p *context) localityAllowsGlobalDebug(global *ssa.Global) bool { diff --git a/cl/native_callback_store_test.go b/cl/native_callback_store_test.go new file mode 100644 index 0000000000..b6e69c7c1d --- /dev/null +++ b/cl/native_callback_store_test.go @@ -0,0 +1,66 @@ +package cl + +import ( + "testing" + + llssa "github.com/xgo-dev/llgo/ssa" + "github.com/xgo-dev/llvm" +) + +func TestNativeCallbackStoreWidth(t *testing.T) { + const source = `package callback +import _ "unsafe" +//go:linkname Callback C.native_callback +var Callback func(int32) int32 +func Reset() { Callback = nil } +func Capture() func() { + saved := Callback + return func() { Callback = saved } +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + for _, target := range []llssa.Target{ + {GOOS: "linux", GOARCH: "amd64"}, + {GOOS: "darwin", GOARCH: "arm64"}, + {GOOS: "windows", GOARCH: "386", LLVMTarget: "i686-pc-windows-msvc"}, + {GOOS: "windows", GOARCH: "amd64", LLVMTarget: "x86_64-pc-windows-msvc"}, + {GOOS: "windows", GOARCH: "arm64", LLVMTarget: "aarch64-pc-windows-msvc"}, + {GOOS: "windows", GOARCH: "386", LLVMTarget: "i686-w64-windows-gnu"}, + {GOOS: "windows", GOARCH: "amd64", LLVMTarget: "x86_64-w64-windows-gnu"}, + {GOOS: "windows", GOARCH: "arm64", LLVMTarget: "aarch64-w64-windows-gnu"}, + } { + t.Run(target.GOOS+"/"+target.GOARCH+"/"+target.LLVMTarget, func(t *testing.T) { + prog := newLLSSAProgForTarget(t, &target) + defer prog.Dispose() + pkg, err := NewPackage(prog, ssaPkg, files) + if err != nil { + t.Fatal(err) + } + mod := pkg.Module() + slot := mod.NamedGlobal("native_callback") + if slot.IsNil() || slot.GlobalValueType().TypeKind() != llvm.PointerTypeKind { + t.Fatal("native callback must occupy one pointer") + } + for _, name := range []string{"callback.Reset", "callback.Capture$1"} { + fn := mustNamedFunction(t, mod, name) + stores := 0 + for block := fn.FirstBasicBlock(); !block.IsNil(); block = llvm.NextBasicBlock(block) { + for inst := block.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if inst.InstructionOpcode() == llvm.Store && inst.Operand(1) == slot { + stores++ + if inst.Operand(0).Type().TypeKind() != llvm.PointerTypeKind { + t.Fatalf("callback store overwrites adjacent native storage:\n%s", fn.String()) + } + } + } + } + if stores != 1 { + t.Fatalf("expected one callback store in %s, got %d", name, stores) + } + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 8809716ba8..4c574ad339 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -1104,8 +1104,8 @@ var Dynamic = [2]int{value(), 9} func Use() int { return Dynamic[0] + Dynamic[1] } ` - ir := compileWithRewrites(t, src, nil) - if !strings.Contains(ir, "@staticinit.Dynamic = global [2 x i64] zeroinitializer") { + ir := compileWithRewritesTarget(t, src, nil, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + if !strings.Contains(ir, "@staticinit.Dynamic = common global [2 x i64] zeroinitializer") { t.Fatalf("dynamic global should keep zero initializer:\n%s", ir) } assertStoreToGlobal(t, ir, "@staticinit.Dynamic") @@ -1122,10 +1122,10 @@ var External = 7 func Use() int { return External } ` ir := compileWithRewrites(t, src, nil) - if !strings.Contains(ir, "@C.external = external global i64") { + if !strings.Contains(ir, "@external = external global i64") { t.Fatalf("linknamed global should remain an external declaration:\n%s", ir) } - assertStoreToGlobal(t, ir, "@C.external") + assertStoreToGlobal(t, ir, "@external") } func TestStaticGlobalInitDeterministicOrder(t *testing.T) { @@ -2473,8 +2473,8 @@ var G = Zero{} func Use() int { return G.Value + G.Array[1] } ` - ir := compileWithRewrites(t, src, nil) - if !strings.Contains(ir, "@staticinit.G = global %staticinit.Zero zeroinitializer") { + ir := compileWithRewritesTarget(t, src, nil, &llssa.Target{GOOS: "linux", GOARCH: "amd64"}) + if !strings.Contains(ir, "@staticinit.G = common global %staticinit.Zero zeroinitializer") { t.Fatalf("missing zero static initializer:\n%s", ir) } assertNoStoreToGlobal(t, ir, "@staticinit.G") diff --git a/cl/static_map_init_test.go b/cl/static_map_init_test.go index f7f25eb451..0a0adbbd0c 100644 --- a/cl/static_map_init_test.go +++ b/cl/static_map_init_test.go @@ -213,7 +213,7 @@ var NotSlice = map[string]int{"a": 1} func Use() int { return Keep["a"][0] + Skip["a"][0] + NotMap + NotSlice["a"] + Rewritten["a"][0] + Linked["a"][0] + __cgo_cb["a"][0] } `) prog := ssatest.NewProgram(t, nil) - prog.SetLinkname("foo.Linked", "bar.Linked") + prog.SetLinkname("foo.Linked", "C.Linked") prog.SetLocalityInfo(llssa.FullName(pkg.Pkg, "Keep"), llssa.LocalityInfo{Locality: llssa.ThreadLocal}) ctx := &context{ prog: prog, @@ -478,7 +478,7 @@ var M = map[string][]int{"a": {1}} func Use() int { return M["a"][0] } `) prog := ssatest.NewProgram(t, nil) - prog.SetLinkname(llssa.FullName(pkg.Pkg, "M"), "bar.M") + prog.SetLinkname(llssa.FullName(pkg.Pkg, "M"), "C.M") ctx := &context{prog: prog, pkg: prog.NewPackage("foo", "foo")} ctx.collectStaticMapInits(pkg) if ctx.staticMapSliceValues != nil { diff --git a/doc/_readme/go.mod b/doc/_readme/go.mod index c0de5e050d..c3ef56912a 100644 --- a/doc/_readme/go.mod +++ b/doc/_readme/go.mod @@ -1,5 +1,8 @@ module github.com/xgo-dev/llgo/doc/_readme -go 1.20 +go 1.23 require github.com/goplus/lib v0.5.1 + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/doc/_readme/go.sum b/doc/_readme/go.sum index 389ae9c56b..3c31f27fbf 100644 --- a/doc/_readme/go.sum +++ b/doc/_readme/go.sum @@ -1,2 +1,2 @@ -github.com/goplus/lib v0.5.1 h1:1I/FA8igyXy4GOrqufmzZNCT0F34m9/BLQlGAl7XvZg= -github.com/goplus/lib v0.5.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= diff --git a/go.mod b/go.mod index 5965665622..1bd7f60086 100644 --- a/go.mod +++ b/go.mod @@ -29,3 +29,6 @@ replace github.com/xgo-dev/llgo/runtime => ./runtime // v0.51.0 plus the generic-method wrapper fix; remove when upstream includes it. replace golang.org/x/tools => github.com/cpunion/tools v0.48.1-0.20261019010317-d793c18ac89b + +// Explicit native variable bindings from goplus/lib#53; remove after its release. +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c diff --git a/go.sum b/go.sum index 58a6dc39a7..1803e33ee7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c h1:pqRvM5cQC1kUble8Wh7XHZcFhDouoMGnqKlWMBeaMS4= +github.com/cpunion/lib v0.0.0-20261021003434-95e56e9fa51c/go.mod h1:0krESx2ZyKMlgNp3oUMob++WJYR5a1PpubjSmMjnP0c= github.com/cpunion/tools v0.48.1-0.20261019010317-d793c18ac89b h1:YdRXrwHMwoWhMBU5/OotFhKQ5goLyxwbcHYgGs/t77k= github.com/cpunion/tools v0.48.1-0.20261019010317-d793c18ac89b/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= diff --git a/internal/build/cgo_pragmas.go b/internal/build/cgo_pragmas.go index c47114debf..34544a37af 100644 --- a/internal/build/cgo_pragmas.go +++ b/internal/build/cgo_pragmas.go @@ -156,6 +156,11 @@ func lowerWindowsCgoImportPointers(goos, goarch, pkgPath string, files []*ast.Fi // require an address-variable initializer. continue } + if init := global.Initializer(); !init.IsNil() && !init.IsNull() { + // Like gc, a static data definition takes precedence over the + // dynamic import. Only undefined/zero storage becomes an IAT slot. + continue + } if global.GlobalValueType().TypeKind() != gllvm.PointerTypeKind { return fmt.Errorf("%s: go:cgo_import_dynamic local %q is not a pointer variable", pkgPath, d.local) } diff --git a/internal/build/linkname_dynamic_test.go b/internal/build/linkname_dynamic_test.go new file mode 100644 index 0000000000..8781af3376 --- /dev/null +++ b/internal/build/linkname_dynamic_test.go @@ -0,0 +1,97 @@ +package build + +import ( + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "testing" + + "github.com/xgo-dev/llgo/cl" + llssa "github.com/xgo-dev/llgo/ssa" + "github.com/xgo-dev/llgo/ssa/ssatest" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestWindowsDynamicImportFromGoSource(t *testing.T) { + const source = `package dynslot +import _ "unsafe" +//go:cgo_import_dynamic dynslot.Default CloseHandle%1 "kernel32.dll" +var Default *byte +//go:cgo_import_dynamic bare_import GetCurrentProcess%0 "kernel32.dll" +//go:linkname Renamed bare_import +var Renamed *byte +` + for _, arch := range []string{"386", "amd64", "arm64"} { + t.Run(arch, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "imports.go", source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + files := []*ast.File{file} + imp := importer.Default() + ssaPkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, + types.NewPackage("dynslot", "dynslot"), files, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + prog := ssatest.NewProgramEx(t, &llssa.Target{GOOS: "windows", GOARCH: arch}, imp) + prog.TypeSizes(types.SizesFor("gc", arch)) + pkg, _, err := cl.NewPackageEx(prog, nil, nil, ssaPkg, files) + if err != nil { + t.Fatal(err) + } + mod := pkg.Module() + for _, name := range []string{"dynslot.Default", "bare_import"} { + v := mod.NamedGlobal(name) + if v.IsNil() || v.GlobalValueType().TypeKind() != llvm.PointerTypeKind || !v.Initializer().IsNil() { + t.Fatalf("%s must reach import lowering as an external pointer: %s", name, mod.String()) + } + } + if err := lowerWindowsCgoImportPointers("windows", arch, "dynslot", files, mod); err != nil { + t.Fatal(err) + } + for local, remote := range map[string]string{"dynslot.Default": "CloseHandle", "bare_import": "GetCurrentProcess"} { + fn := mod.NamedFunction(remote) + if fn.IsNil() || fn.DLLStorageClass() != llvm.DLLImportStorageClass || mod.NamedGlobal(local).Initializer() != fn { + t.Fatalf("%s does not contain the imported address: %s", local, mod.String()) + } + want := llvm.CCallConv + if arch == "386" { + want = llvm.X86StdcallCallConv + } + if fn.FunctionCallConv() != want { + t.Fatalf("%s has wrong call convention", remote) + } + } + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestWindowsDynamicImportPreservesStaticDefinition(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + mod := ctx.NewModule("static-import") + defer mod.Dispose() + ptr := llvm.PointerType(ctx.Int8Type(), 0) + global := llvm.AddGlobal(mod, ptr, "dynslot.Value") + initial := llvm.ConstIntToPtr(llvm.ConstInt(ctx.Int64Type(), 42, false), ptr) + global.SetInitializer(initial) + file, err := parser.ParseFile(token.NewFileSet(), "imports.go", "package dynslot\n//go:cgo_import_dynamic dynslot.Value CloseHandle%1 \"kernel32.dll\"\n", parser.ParseComments) + if err != nil { + t.Fatal(err) + } + if err := lowerWindowsCgoImportPointers("windows", "amd64", "dynslot", []*ast.File{file}, mod); err != nil { + t.Fatal(err) + } + if global.Initializer() != initial || !mod.NamedFunction("CloseHandle").IsNil() { + t.Fatal("dynamic import replaced a static definition") + } +} diff --git a/internal/build/linkname_storage_test.go b/internal/build/linkname_storage_test.go new file mode 100644 index 0000000000..fbd50722e4 --- /dev/null +++ b/internal/build/linkname_storage_test.go @@ -0,0 +1,22 @@ +package build + +import ( + "path/filepath" + "strings" + "testing" +) + +func TestLinknameDuplicateStaticVariables(t *testing.T) { + conf := NewDefaultConf(ModeBuild) + conf.OutFile = filepath.Join(t.TempDir(), "duplicate") + output := captureStderr(t) + _, err := Do([]string{"../../test/go/testdata/linknamevars/duplicate"}, conf) + if err == nil { + t.Fatal("two nonzero definitions of one linkname unexpectedly linked") + } + message := strings.ToLower(output()) + if !strings.Contains(message, "llgo_test_duplicate_data") || + !(strings.Contains(message, "duplicate") || strings.Contains(message, "multiply defined")) { + t.Fatalf("expected duplicate-definition diagnostic for the shared symbol, got: %v\n%s", err, message) + } +} diff --git a/runtime/internal/clite/c.go b/runtime/internal/clite/c.go index 3ceeca680c..15be7db487 100644 --- a/runtime/internal/clite/c.go +++ b/runtime/internal/clite/c.go @@ -275,22 +275,22 @@ type Option struct { Val Int } -//go:linkname Argc __llgo_argc +//go:linkname Argc C.__llgo_argc var Argc Int -//go:linkname Argv __llgo_argv +//go:linkname Argv C.__llgo_argv var Argv **Char -//go:linkname Optarg optarg +//go:linkname Optarg C.optarg var Optarg *Char -//go:linkname Optind optind +//go:linkname Optind C.optind var Optind Int -//go:linkname Opterr opterr +//go:linkname Opterr C.opterr var Opterr Int -//go:linkname Optopt optopt +//go:linkname Optopt C.optopt var Optopt Int //go:linkname Getopt C.getopt diff --git a/runtime/internal/clite/os/os.go b/runtime/internal/clite/os/os.go index e0cca8582e..cf2e5f6030 100644 --- a/runtime/internal/clite/os/os.go +++ b/runtime/internal/clite/os/os.go @@ -108,7 +108,7 @@ func Chroot(path *c.Char) c.Int // ----------------------------------------------------------------------------- -//go:linkname Environ environ +//go:linkname Environ C.environ var Environ **c.Char //go:linkname Getenv C.getenv diff --git a/runtime/internal/clite/stdio_darwin.go b/runtime/internal/clite/stdio_darwin.go index d61eae8ecd..d0b81dfe8f 100644 --- a/runtime/internal/clite/stdio_darwin.go +++ b/runtime/internal/clite/stdio_darwin.go @@ -25,11 +25,11 @@ const ( LLGoPackage = "decl" ) -//go:linkname Stdin __stdinp +//go:linkname Stdin C.__stdinp var Stdin FilePtr -//go:linkname Stdout __stdoutp +//go:linkname Stdout C.__stdoutp var Stdout FilePtr -//go:linkname Stderr __stderrp +//go:linkname Stderr C.__stderrp var Stderr FilePtr diff --git a/runtime/internal/clite/stdio_default.go b/runtime/internal/clite/stdio_default.go index 3c70ba1ca4..608efab0ec 100644 --- a/runtime/internal/clite/stdio_default.go +++ b/runtime/internal/clite/stdio_default.go @@ -24,11 +24,11 @@ const ( LLGoPackage = "decl" ) -//go:linkname Stdin stdin +//go:linkname Stdin C.stdin var Stdin FilePtr -//go:linkname Stdout stdout +//go:linkname Stdout C.stdout var Stdout FilePtr -//go:linkname Stderr stderr +//go:linkname Stderr C.stderr var Stderr FilePtr diff --git a/runtime/internal/gcroot/current_wasm.go b/runtime/internal/gcroot/current_wasm.go index 9d72691d7a..0ecf7a4ca8 100644 --- a/runtime/internal/gcroot/current_wasm.go +++ b/runtime/internal/gcroot/current_wasm.go @@ -7,10 +7,10 @@ import _ "unsafe" // Keep the chain head pointer-free so its boundary helpers do not acquire a // compiler root frame while they are changing the chain itself. // -//go:linkname currentRootChain llvm_gc_root_chain +//go:linkname currentRootChain C.llvm_gc_root_chain var currentRootChain uintptr -//go:linkname sjljReplaying llvm_gc_root_sjlj_replaying +//go:linkname sjljReplaying C.llvm_gc_root_sjlj_replaying var sjljReplaying bool var activeContext uintptr diff --git a/runtime/internal/lib/runtime/link_darwin_llgo.go b/runtime/internal/lib/runtime/link_darwin_llgo.go index 6545a33b1b..24c0f1683b 100644 --- a/runtime/internal/lib/runtime/link_darwin_llgo.go +++ b/runtime/internal/lib/runtime/link_darwin_llgo.go @@ -53,7 +53,7 @@ func os_runtime_args() []string { return args } -//go:linkname c_environ environ +//go:linkname c_environ C.environ var c_environ **c.Char //go:linkname syscall_runtime_envs syscall.runtime_envs diff --git a/runtime/internal/lib/runtime/link_linux_llgo.go b/runtime/internal/lib/runtime/link_linux_llgo.go index 5b319e5b48..786c1cd3b1 100644 --- a/runtime/internal/lib/runtime/link_linux_llgo.go +++ b/runtime/internal/lib/runtime/link_linux_llgo.go @@ -61,7 +61,7 @@ func os_runtime_args() []string { return args } -//go:linkname c_environ environ +//go:linkname c_environ C.environ var c_environ **c.Char //go:linkname syscall_runtime_envs syscall.runtime_envs diff --git a/runtime/internal/lib/runtime/pclntab_external.go b/runtime/internal/lib/runtime/pclntab_external.go index 4c00dc5665..3123c9f43f 100644 --- a/runtime/internal/lib/runtime/pclntab_external.go +++ b/runtime/internal/lib/runtime/pclntab_external.go @@ -70,7 +70,7 @@ const ( externalPCLNUnavailable ) -//go:linkname runtimePCLNIdentity __llgo_pclntab_identity +//go:linkname runtimePCLNIdentity C.__llgo_pclntab_identity var runtimePCLNIdentity [32]byte var externalPCLNState uint32 diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index dea10b37e0..42ef89223d 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -156,37 +156,37 @@ const ( runtimeFuncInfoLineMask = runtimeFuncInfoLineWrapper - 1 ) -//go:linkname runtimeFuncInfoTable __llgo_funcinfo_table +//go:linkname runtimeFuncInfoTable C.__llgo_funcinfo_table var runtimeFuncInfoTable *runtimeFuncInfoRecord -//go:linkname runtimeFuncInfoStrings __llgo_funcinfo_strings +//go:linkname runtimeFuncInfoStrings C.__llgo_funcinfo_strings var runtimeFuncInfoStrings *c.Char -//go:linkname runtimeFuncInfoStringOffsets __llgo_funcinfo_string_offsets +//go:linkname runtimeFuncInfoStringOffsets C.__llgo_funcinfo_string_offsets var runtimeFuncInfoStringOffsets *uint32 -//go:linkname runtimeFuncInfoStringCount __llgo_funcinfo_string_count +//go:linkname runtimeFuncInfoStringCount C.__llgo_funcinfo_string_count var runtimeFuncInfoStringCount uintptr -//go:linkname runtimeFuncInfoHash __llgo_funcinfo_hash +//go:linkname runtimeFuncInfoHash C.__llgo_funcinfo_hash var runtimeFuncInfoHash *uint16 -//go:linkname runtimeFuncInfoCount __llgo_funcinfo_count +//go:linkname runtimeFuncInfoCount C.__llgo_funcinfo_count var runtimeFuncInfoCount uintptr -//go:linkname runtimeFuncInfoHashMask __llgo_funcinfo_hash_mask +//go:linkname runtimeFuncInfoHashMask C.__llgo_funcinfo_hash_mask var runtimeFuncInfoHashMask uintptr -//go:linkname runtimeFuncInfoSymbolIndex __llgo_funcinfo_symbol_index +//go:linkname runtimeFuncInfoSymbolIndex C.__llgo_funcinfo_symbol_index var runtimeFuncInfoSymbolIndex *runtimeFuncInfoSymbolIndexRecord -//go:linkname runtimeFuncInfoSymbolIndexCount __llgo_funcinfo_symbol_index_count +//go:linkname runtimeFuncInfoSymbolIndexCount C.__llgo_funcinfo_symbol_index_count var runtimeFuncInfoSymbolIndexCount uintptr -//go:linkname runtimeFuncInfoEntryStart __llgo_funcinfo_entry_start +//go:linkname runtimeFuncInfoEntryStart C.__llgo_funcinfo_entry_start var runtimeFuncInfoEntryStart *runtimeFuncInfoEntryRecord -//go:linkname runtimeFuncInfoEntryEnd __llgo_funcinfo_entry_end +//go:linkname runtimeFuncInfoEntryEnd C.__llgo_funcinfo_entry_end var runtimeFuncInfoEntryEnd *runtimeFuncInfoEntryRecord type runtimePCLineRecord struct { @@ -197,16 +197,16 @@ type runtimePCLineRecord struct { line uint32 } -//go:linkname runtimePCLineTable __llgo_pcline_table +//go:linkname runtimePCLineTable C.__llgo_pcline_table var runtimePCLineTable *runtimePCLineRecord -//go:linkname runtimePCLineCount __llgo_pcline_count +//go:linkname runtimePCLineCount C.__llgo_pcline_count var runtimePCLineCount uintptr -//go:linkname runtimePCSiteStart __llgo_pcsite_start +//go:linkname runtimePCSiteStart C.__llgo_pcsite_start var runtimePCSiteStart *runtimePCSiteRecord -//go:linkname runtimePCSiteEnd __llgo_pcsite_end +//go:linkname runtimePCSiteEnd C.__llgo_pcsite_end var runtimePCSiteEnd *runtimePCSiteRecord type runtimePCLineFrame struct { diff --git a/runtime/internal/lib/runtime/unwind_llgo.go b/runtime/internal/lib/runtime/unwind_llgo.go index 48e9ec17b2..7e240f4b8c 100644 --- a/runtime/internal/lib/runtime/unwind_llgo.go +++ b/runtime/internal/lib/runtime/unwind_llgo.go @@ -252,7 +252,7 @@ const maxFPStride = 1 << 20 // functions were compiled with the frame-pointer attribute // (ssa.Program.NeedsFramePointer). // -//go:linkname runtimeFPChain __llgo_fp_chain +//go:linkname runtimeFPChain C.__llgo_fp_chain var runtimeFPChain uint8 // fpUnwindAvailable reports whether the physical walk can be used for the diff --git a/runtime/internal/runtime/proc_wasm.go b/runtime/internal/runtime/proc_wasm.go index 2d8d27286c..c9774f8ed5 100644 --- a/runtime/internal/runtime/proc_wasm.go +++ b/runtime/internal/runtime/proc_wasm.go @@ -88,7 +88,7 @@ func initWasmScheduler(gp *g) { wasmSched.started = true } -//go:linkname wasmMainTask __llgo_wasm_main +//go:linkname wasmMainTask C.__llgo_wasm_main func wasmMainTask(unsafe.Pointer) unsafe.Pointer // RunWasmMain keeps host waits on the original Emscripten system context and diff --git a/runtime/internal/runtime/proc_wasm_workers.go b/runtime/internal/runtime/proc_wasm_workers.go index 6473f68fec..93ec1e161b 100644 --- a/runtime/internal/runtime/proc_wasm_workers.go +++ b/runtime/internal/runtime/proc_wasm_workers.go @@ -158,7 +158,7 @@ func initWasmScheduler(gp *g) { } } -//go:linkname wasmMainTask __llgo_wasm_main +//go:linkname wasmMainTask C.__llgo_wasm_main func wasmMainTask(unsafe.Pointer) unsafe.Pointer func RunWasmMain() { diff --git a/runtime/internal/runtime/tinygogc/gc_link.go b/runtime/internal/runtime/tinygogc/gc_link.go index 550341e897..8ee4e4b8ef 100644 --- a/runtime/internal/runtime/tinygogc/gc_link.go +++ b/runtime/internal/runtime/tinygogc/gc_link.go @@ -32,22 +32,22 @@ func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { return Realloc(ptr, size) } -//go:linkname _heapStart _heapStart +//go:linkname _heapStart C._heapStart var _heapStart [0]byte -//go:linkname _heapEnd _heapEnd +//go:linkname _heapEnd C._heapEnd var _heapEnd [0]byte -//go:linkname _stackStart _stack_top +//go:linkname _stackStart C._stack_top var _stackStart [0]byte -//go:linkname _stackEnd _stack_end +//go:linkname _stackEnd C._stack_end var _stackEnd [0]byte -//go:linkname _globals_start _globals_start +//go:linkname _globals_start C._globals_start var _globals_start [0]byte -//go:linkname _globals_end _globals_end +//go:linkname _globals_end C._globals_end var _globals_end [0]byte func gcMemoryLayout() (heapStart, heapEnd, globalsStart, globalsEnd, stackTop uintptr) { diff --git a/ssa/backend_program_test.go b/ssa/backend_program_test.go index 585a4798d7..0b5ed9a101 100644 --- a/ssa/backend_program_test.go +++ b/ssa/backend_program_test.go @@ -41,6 +41,7 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { pkg := types.NewPackage("example.com/p", "p") fset := token.NewFileSet() coordinator.SetLinkname("example.com/p.Entry", "entry") + coordinator.SetExternalImport("external") coordinator.SetPackageExport("example.com/p.Entry", "entry") coordinator.SetNoInterfaceMethod("example.com/p.T.Hidden") coordinator.SetTypeCallConv("example.com/p.CType", CDecl) @@ -69,6 +70,9 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { if !backend.HasLinknameTarget("entry") || backend.HasLinknameTarget("missing") { t.Fatal("backend Program did not preserve linkname target lookup") } + if !backend.IsExternalImport("external") || backend.IsExternalImport("missing") { + t.Fatal("backend lost the build-wide import symbol set") + } if export, ok := backend.PackageExport("example.com/p.Entry"); !ok || export != "entry" { t.Fatalf("PackageExport = (%q, %v), want (entry, true)", export, ok) } diff --git a/ssa/decl.go b/ssa/decl.go index ab3eaef9f5..c56f9fd531 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -199,7 +199,7 @@ func (p Package) doNewVar(name string, t Type) Global { func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { elem := p.Prog.Elem(t) typ := p.Prog.storageType(elem) - if !threadLocal && p.Prog.td.TypeAllocSize(typ) == 0 { + if !threadLocal && p.Prog.td.TypeAllocSize(typ) == 0 && !p.Prog.HasLinknameTarget(name) { var rt *types.Package if p.Prog.rt != nil || p.Prog.rtget != nil { rt = p.Prog.runtime() @@ -272,6 +272,44 @@ func (g Global) InitNil() { g.impl.SetInitializer(llvm.ConstNull(g.impl.GlobalValueType())) } +// InitSharedGo defines ordinary package-level Go storage, including variables +// referenced through linkname from a separately compiled package. +// Zero definitions coalesce; a nonzero static initializer overrides them. +// Keep runtime initialization in each package's init function. A cgo +// import may replace zero storage, but never a nonzero static definition. +func (g Global) InitSharedGo(v Expr, externalImport bool) error { + if g.isZeroSizedAlias { + return nil + } + init := llvm.ConstNull(g.impl.GlobalValueType()) + if !v.IsNil() { + init = g.prog.toStorageConstant(g.prog.Elem(g.Type), v.impl) + } + previous := g.impl.Initializer() + if !previous.IsNil() && !previous.IsNull() { + if !init.IsNull() { + return fmt.Errorf("duplicate definition of Go variable symbol %q", g.impl.Name()) + } + return nil + } + if init.IsNull() && externalImport { + return nil + } + g.impl.SetInitializer(init) + linkage := llvm.ExternalLinkage + if init.IsNull() { + linkage = llvm.CommonLinkage + if target := g.prog.Target(); target.GOARCH == "wasm" || target.GOOS == "darwin" { + // Wasm cannot emit common symbols. Mach-O common definitions + // lose their DWARF address relocations during debug linking. + // Weak definitions coalesce zeros and yield to static data on both. + linkage = llvm.WeakAnyLinkage + } + } + g.impl.SetLinkage(linkage) + return nil +} + func (g Global) ReplaceAllUsesWith(v Expr) { g.impl.ReplaceAllUsesWith(v.impl) } diff --git a/ssa/linkname_storage_test.go b/ssa/linkname_storage_test.go new file mode 100644 index 0000000000..d02a6df2dd --- /dev/null +++ b/ssa/linkname_storage_test.go @@ -0,0 +1,93 @@ +package ssa + +import ( + "go/types" + "strings" + "testing" + + "github.com/xgo-dev/llvm" +) + +func TestLinknameTargetRebinding(t *testing.T) { + prog := NewProgram(nil) + defer prog.Dispose() + prog.SetLinkname("p.A", "shared") + prog.SetLinkname("p.B", "shared") + prog.SetLinkname("p.A", "shared") // Idempotent updates must not leak counts. + prog.SetLinkname("p.A", "other") + if !prog.HasLinknameTarget("shared") || !prog.HasLinknameTarget("other") { + t.Fatal("rebinding one alias removed a live target") + } + prog.SetLinkname("p.B", "other") + if prog.HasLinknameTarget("shared") || !prog.HasLinknameTarget("other") { + t.Fatal("last alias did not remove the old target") + } + if target, ok := prog.Linkname("p.A"); !ok || target != "other" { + t.Fatalf("forward index = %q, %v", target, ok) + } +} + +func TestSharedGoStorageDefinitions(t *testing.T) { + for _, target := range []*Target{{GOOS: "linux", GOARCH: "amd64"}, {GOOS: "darwin", GOARCH: "arm64"}, {GOOS: "wasip1", GOARCH: "wasm"}} { + t.Run(target.GOARCH, func(t *testing.T) { + prog := NewProgram(target) + defer prog.Dispose() + pkg := prog.NewPackage("storage", "storage") + ptr := types.NewPointer(types.Typ[types.Int32]) + strong := prog.IntVal(41, prog.Type(types.Typ[types.Int32], GoDecl)) + for _, order := range []string{"zero-first", "static-first"} { + t.Run(order, func(t *testing.T) { + g := pkg.NewVar(order, ptr, GoDecl) + values := []Expr{{}, strong} + if order == "static-first" { + values[0], values[1] = values[1], values[0] + } + for _, value := range values { + if err := g.InitSharedGo(value, false); err != nil { + t.Fatal(err) + } + } + if g.impl.Initializer().ZExtValue() != 41 || g.impl.Linkage() != llvm.ExternalLinkage { + t.Fatal("zero storage overrode the static definition") + } + if err := g.InitSharedGo(strong, false); err == nil || !strings.Contains(err.Error(), order) { + t.Fatalf("duplicate static definition diagnostic = %v", err) + } + }) + } + zero := pkg.NewVar("zero", ptr, GoDecl) + for range 2 { + if err := zero.InitSharedGo(Expr{}, false); err != nil { + t.Fatal(err) + } + } + want := llvm.CommonLinkage + if target.GOARCH == "wasm" || target.GOOS == "darwin" { + want = llvm.WeakAnyLinkage + } + if zero.impl.Initializer().IsNil() || !zero.impl.Initializer().IsNull() || zero.impl.Linkage() != want { + t.Fatal("zero declarations must define coalescible storage") + } + imported := pkg.NewVar("imported", ptr, GoDecl) + if err := imported.InitSharedGo(Expr{}, true); err != nil { + t.Fatal(err) + } + if !imported.impl.Initializer().IsNil() { + t.Fatal("cgo import acquired zero storage") + } + if err := imported.InitSharedGo(strong, true); err != nil { + t.Fatal(err) + } + if imported.impl.Initializer().ZExtValue() != 41 { + t.Fatal("cgo import replaced static data") + } + empty := pkg.NewVar("empty", types.NewPointer(types.NewStruct(nil, nil)), GoDecl) + if err := empty.InitSharedGo(Expr{}, false); err != nil { + t.Fatal(err) + } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/ssa/memory.go b/ssa/memory.go index 1cfc66886b..26fdbf752b 100644 --- a/ssa/memory.go +++ b/ssa/memory.go @@ -507,9 +507,17 @@ func (b Builder) Load(ptr Expr) Expr { func (b Builder) Store(ptr, val Expr) Expr { raw := ptr.raw.Type dbgInstrf("Store %v, %v, %v\n", raw, ptr.impl, val.impl) - val = checkExpr(val, raw.(*types.Pointer).Elem(), b) - b.assertStaticNilDeref(ptr) storageType := b.Prog.Elem(ptr.Type) + if storageType.kind == vkFuncPtr && val.kind == vkClosure { + // The raw signature alone does not distinguish a C pointer slot from + // a Go funcval. Use the actual storage representation so a two-word + // Go value cannot overwrite the next native variable. ChangeType also + // rejects closure environments that a native callback cannot carry. + val = b.ChangeType(storageType, val) + } else { + val = checkExpr(val, raw.(*types.Pointer).Elem(), b) + } + b.assertStaticNilDeref(ptr) if storageType.kind == vkSIMD { stored := b.impl.CreateStore(val.impl, ptr.impl) stored.SetAlignment(int(b.Prog.AlignOf(storageType))) diff --git a/ssa/package.go b/ssa/package.go index 929ceed39d..479848bd95 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -510,7 +510,7 @@ func (p Program) SetTypeCallConv(typFullName string, cc CallConv) { func (p Program) SetLinkname(name, link string) { p.packageSyntax.mu.Lock() - p.packageSyntax.linknames[name] = link + p.packageSyntax.setLinkname(name, link) p.packageSyntax.mu.Unlock() } @@ -546,12 +546,7 @@ func (p Program) WasmImport(name string) (module, importName string, ok bool) { func (p Program) HasLinknameTarget(target string) bool { p.packageSyntax.mu.RLock() defer p.packageSyntax.mu.RUnlock() - for _, link := range p.packageSyntax.linknames { - if link == target { - return true - } - } - return false + return p.packageSyntax.linknameTargets[target] != 0 } type closureEnvDirectiveKey struct { diff --git a/ssa/package_syntax.go b/ssa/package_syntax.go index b2949090cf..af10e132da 100644 --- a/ssa/package_syntax.go +++ b/ssa/package_syntax.go @@ -28,6 +28,8 @@ import ( type packageSyntaxData struct { mu sync.RWMutex linknames map[string]string + linknameTargets map[string]int + externalImports map[string]none wasmImports map[string]wasmImport exports map[string]string closureEnvDirectives map[closureEnvDirectiveKey]none @@ -39,6 +41,8 @@ type packageSyntaxData struct { func newPackageSyntaxData() *packageSyntaxData { return &packageSyntaxData{ linknames: make(map[string]string), + linknameTargets: make(map[string]int), + externalImports: make(map[string]none), wasmImports: make(map[string]wasmImport), exports: make(map[string]string), closureEnvDirectives: make(map[closureEnvDirectiveKey]none), @@ -53,6 +57,23 @@ type wasmImport struct { name string } +// SetExternalImport records the literal local symbol of a cgo import pragma. +// Imports can name ordinary package variables without any linkname directive. +// The namespace is build-wide: gc also looks up cgo locals by literal linker +// symbol (version 0), so an import can replace eligible BSS in another package. +func (p Program) SetExternalImport(name string) { + p.packageSyntax.mu.Lock() + p.packageSyntax.externalImports[name] = none{} + p.packageSyntax.mu.Unlock() +} + +func (p Program) IsExternalImport(name string) bool { + p.packageSyntax.mu.RLock() + _, ok := p.packageSyntax.externalImports[name] + p.packageSyntax.mu.RUnlock() + return ok +} + // SetPackageExport records an export directive before the LLVM Package that // will later preserve the symbol exists. func (p Program) SetPackageExport(name, export string) { @@ -95,3 +116,16 @@ func (p *packageSyntaxData) typeCallConv(name string) (CallConv, bool) { p.mu.RUnlock() return background, ok } + +// setLinkname updates both indexes while the caller holds mu. Counts preserve a +// target shared by multiple declarations when one declaration is rebound. +func (p *packageSyntaxData) setLinkname(name, target string) { + if previous, ok := p.linknames[name]; ok { + p.linknameTargets[previous]-- + if p.linknameTargets[previous] == 0 { + delete(p.linknameTargets, previous) + } + } + p.linknames[name] = target + p.linknameTargets[target]++ +} diff --git a/test/cgo/hfa_arm64.go b/test/cgo/hfa_arm64.go index e73ae63018..56d885d9ef 100644 --- a/test/cgo/hfa_arm64.go +++ b/test/cgo/hfa_arm64.go @@ -12,7 +12,7 @@ import "unsafe" //llgo:cdecl type hfaPairEntry func(float64, float64, float64, float64, float64, float64, float64, float64, C.llgo_hfa_pair, float64) float64 -//go:linkname linkedHFAPair llgo_test_hfa_pair +//go:linkname linkedHFAPair C.llgo_test_hfa_pair func linkedHFAPair(float64, float64, float64, float64, float64, float64, float64, float64, C.llgo_hfa_pair, float64) float64 func directHFAPair() float64 { diff --git a/test/cgo/linkname_vars.c b/test/cgo/linkname_vars.c new file mode 100644 index 0000000000..0f02529811 --- /dev/null +++ b/test/cgo/linkname_vars.c @@ -0,0 +1,30 @@ +//go:build llgo && !wasm + +#include "linkname_vars.h" + +static int32_t twice(int32_t value) { return 2 * value; } + +int32_t llgo_linkvar_scalar = 42; +int32_t llgo_linkvar_array[3] = {3, 5, 7}; +int32_t *llgo_linkvar_pointer = &llgo_linkvar_scalar; +int32_t (*llgo_linkvar_callback)(int32_t) = twice; +int32_t (*llgo_linkvar_callbacks[2])(int32_t) = {twice, twice}; + +void llgo_linkvar_reset_callbacks(void) { + llgo_linkvar_callbacks[0] = twice; + llgo_linkvar_callbacks[1] = twice; +} + +int32_t llgo_linkvar_call(int32_t value) { + return llgo_linkvar_callback(value); +} + +int llgo_linkvar_addresses(void *scalar, void *array, void *pointer, void *callback) { + return scalar == &llgo_linkvar_scalar && + array == &llgo_linkvar_array && + pointer == &llgo_linkvar_pointer && + callback == &llgo_linkvar_callback; +} + +int32_t llgo_linkvar_read_scalar(void) { return llgo_linkvar_scalar; } +void llgo_linkvar_write_array(int32_t value) { llgo_linkvar_array[1] = value; } diff --git a/test/cgo/linkname_vars.go b/test/cgo/linkname_vars.go new file mode 100644 index 0000000000..40bb4f2ee9 --- /dev/null +++ b/test/cgo/linkname_vars.go @@ -0,0 +1,50 @@ +//go:build llgo && !wasm + +package cgo + +/* +#include "linkname_vars.h" +*/ +import "C" + +import "unsafe" + +//go:linkname linkedScalar C.llgo_linkvar_scalar +var linkedScalar int32 + +//llgo:link linkedScalarAlias C.llgo_linkvar_scalar +var linkedScalarAlias int32 + +//go:linkname linkedArray C.llgo_linkvar_array +var linkedArray [3]int32 + +//go:linkname linkedPointer C.llgo_linkvar_pointer +var linkedPointer *int32 + +//go:linkname linkedCallback C.llgo_linkvar_callback +var linkedCallback func(int32) int32 + +//go:linkname linkedCallbacks C.llgo_linkvar_callbacks +var linkedCallbacks [2]func(int32) int32 + +func resetLinkedCallbacks() { + C.llgo_linkvar_reset_callbacks() +} + +func linkedVarAddressesMatch() bool { + return C.llgo_linkvar_addresses( + unsafe.Pointer(&linkedScalar), unsafe.Pointer(&linkedArray), + unsafe.Pointer(&linkedPointer), unsafe.Pointer(&linkedCallback)) != 0 +} + +func linkedVarReadScalar() int32 { + return int32(C.llgo_linkvar_read_scalar()) +} + +func linkedVarWriteArray(value int32) { + C.llgo_linkvar_write_array(C.int32_t(value)) +} + +func linkedVarCall(value int32) int32 { + return int32(C.llgo_linkvar_call(C.int32_t(value))) +} diff --git a/test/cgo/linkname_vars.h b/test/cgo/linkname_vars.h new file mode 100644 index 0000000000..224f873f47 --- /dev/null +++ b/test/cgo/linkname_vars.h @@ -0,0 +1,18 @@ +#ifndef LLGO_TEST_LINKNAME_VARS_H +#define LLGO_TEST_LINKNAME_VARS_H + +#include + +extern int32_t llgo_linkvar_scalar; +extern int32_t llgo_linkvar_array[3]; +extern int32_t *llgo_linkvar_pointer; +extern int32_t (*llgo_linkvar_callback)(int32_t); +extern int32_t (*llgo_linkvar_callbacks[2])(int32_t); + +int32_t llgo_linkvar_call(int32_t value); +int llgo_linkvar_addresses(void *scalar, void *array, void *pointer, void *callback); +int32_t llgo_linkvar_read_scalar(void); +void llgo_linkvar_write_array(int32_t value); +void llgo_linkvar_reset_callbacks(void); + +#endif diff --git a/test/cgo/linkname_vars_test.go b/test/cgo/linkname_vars_test.go new file mode 100644 index 0000000000..90ff6ef821 --- /dev/null +++ b/test/cgo/linkname_vars_test.go @@ -0,0 +1,87 @@ +//go:build llgo && !wasm + +package cgo + +import "testing" + +func TestLinknameCVariables(t *testing.T) { + if !linkedVarAddressesMatch() { + t.Fatal("C-prefixed bindings do not share the native variables' addresses") + } + if &linkedScalarAlias != &linkedScalar { + t.Fatal("llgo:link and go:linkname aliases do not share the same variable address") + } + if linkedScalar != 42 || linkedScalarAlias != 42 { + t.Fatalf("native scalar initializer lost: %d, %d", linkedScalar, linkedScalarAlias) + } + if linkedArray != [3]int32{3, 5, 7} { + t.Fatalf("native array initializer lost: %v", linkedArray) + } + if linkedPointer != &linkedScalar { + t.Fatal("native pointer does not point to the bound scalar") + } + t.Cleanup(func() { + linkedScalar = 42 + linkedArray[1] = 5 + }) + *linkedPointer = 91 + if linkedScalarAlias != 91 || linkedVarReadScalar() != 91 { + t.Fatal("Go writes are not visible through the alias and C") + } + linkedVarWriteArray(11) + if linkedArray[1] != 11 { + t.Fatal("C writes are not visible through the array binding") + } + if linkedCallback == nil { + t.Fatal("native callback initializer lost") + } + if got := linkedCallback(21); got != 42 { + t.Fatalf("native function pointer returned %d, want 42", got) + } + callback := linkedCallback + linkedCallback = nil + if linkedCallback != nil { + t.Fatal("native function pointer could not be cleared") + } + linkedCallback = callback + if got := linkedVarCall(23); got != 46 { + t.Fatalf("restored function pointer returned %d through C, want 46", got) + } +} + +func TestLinknameCVariableCallbackStore(t *testing.T) { + resetLinkedCallbacks() + t.Cleanup(resetLinkedCallbacks) + // Each native slot is one word. Adjacent slots make an oversized Go + // funcval store fail regardless of the linker's global variable order. + saved := linkedCallbacks[0] + restore := func() { linkedCallbacks[0] = saved } + linkedCallbacks[0] = nil + if linkedCallbacks[1] == nil { + t.Fatal("clearing a native callback overwrote the adjacent slot") + } + restore() + for i, callback := range linkedCallbacks { + if callback == nil { + t.Fatalf("restoring a native callback overwrote slot %d", i) + } + if got := callback(21); got != 42 { + t.Fatalf("callback slot %d returned %d, want 42", i, got) + } + } +} + +func TestLinknameCVariableCallbackRejectsClosure(t *testing.T) { + resetLinkedCallbacks() + t.Cleanup(resetLinkedCallbacks) + value := int32(7) + defer func() { + if recover() == nil { + t.Error("capturing Go closure accepted in a native callback slot") + } + if got := linkedCallbacks[0](21); got != 42 { + t.Errorf("rejected assignment changed the native callback: %d", got) + } + }() + linkedCallbacks[0] = func(v int32) int32 { return v + value } +} diff --git a/test/cgo/sysv_aggregate.go b/test/cgo/sysv_aggregate.go index 40fb429046..acdf41012e 100644 --- a/test/cgo/sysv_aggregate.go +++ b/test/cgo/sysv_aggregate.go @@ -22,7 +22,7 @@ import "unsafe" //llgo:cdecl type sysVFloatPairEntry func(float64, float64, float64, float64, float64, float64, float64, float64, C.llgo_sysv_float_pair, float64) float64 -//go:linkname linkedSysVFloatPair llgo_test_sysv_float_pair +//go:linkname linkedSysVFloatPair C.llgo_test_sysv_float_pair func linkedSysVFloatPair(float64, float64, float64, float64, float64, float64, float64, float64, C.llgo_sysv_float_pair, float64) float64 func directSysVFloatPair() float64 { diff --git a/test/go/linkname_storage_test.go b/test/go/linkname_storage_test.go new file mode 100644 index 0000000000..485f58b1c5 --- /dev/null +++ b/test/go/linkname_storage_test.go @@ -0,0 +1,114 @@ +package gotest + +import ( + "reflect" + "testing" + _ "unsafe" + + "github.com/xgo-dev/llgo/test/go/testdata/linknamevars/a" + "github.com/xgo-dev/llgo/test/go/testdata/linknamevars/b" +) + +// Ordinary linkname behavior is shared by gc and LLGo. Native prefixes belong +// in test/llgoext, not in this compatibility suite. +// +//go:linkname linknameStandalone llgo_test_standalone_zero +var linknameStandalone int + +//go:linkname linknameExplicitZero llgo_test_explicit_zero +var linknameExplicitZero = 0 + +//go:linkname linknameGoBody llgo_test_go_body +func linknameGoBody(f func(int) int, n int) int { return f(n) } + +//go:linkname linknameGoReference llgo_test_go_body +func linknameGoReference(func(int) int, int) int + +func TestLinknameGoFunctionDefinitionAndReference(t *testing.T) { + n := 30 + f := func(x int) int { return n + x } + if got := linknameGoReference(f, 12); got != 42 { + t.Fatalf("Go callback result = %d", got) + } +} + +func TestLinknameZeroDefinitions(t *testing.T) { + if linknameStandalone != 0 || linknameExplicitZero != 0 { + t.Fatal("standalone definitions are not zero initialized") + } + if &a.Zero != &b.Zero { + t.Fatal("zero definitions have different storage") + } + old := a.Zero + t.Cleanup(func() { a.Zero = old }) + a.Zero = 71 + if b.Zero != 71 { + t.Fatalf("shared zero value = %d", b.Zero) + } + if &a.Empty != &b.Empty { + t.Fatal("zero-sized linknames have different addresses") + } +} + +func TestLinknameStaticAndDynamicInitialization(t *testing.T) { + if &a.Default != &b.Default || a.Default != 55 || a.DefaultCopied != 55 { + t.Fatal("unannotated zero definition overrode static data") + } + if &a.Static != &b.Static || a.Static != 41 { + t.Fatal("nonzero definition did not override zero storage") + } + if &a.Dynamic != &b.Dynamic || a.Dynamic != 18 || !reflect.DeepEqual(a.Order, []int{1, 2}) { + t.Fatalf("dynamic value/order = %d/%v", a.Dynamic, a.Order) + } + if &a.Text != &b.Text || a.Text != "linked value" || a.Copied != "linked value" { + t.Fatalf("string initialization/copy = %q/%q", a.Text, a.Copied) + } + if &a.Marked != &b.Marked || b.Marked != 19 { + t.Fatal("one-argument marker lost its package symbol") + } + if &a.Named != &b.Named || a.Named != 23 { + t.Fatal("qualified target lost its definition") + } +} + +func TestLinknameGoValueRepresentation(t *testing.T) { + if &a.Closure != &b.Closure || a.Closure == nil { + t.Fatal("closure storage is not shared") + } + // Invoke the initializer-created closure through both aliases before + // replacing it, checking that its captured state is shared as well. + if got := a.Closure(2); got != 22 { + t.Fatalf("initialized closure = %d", got) + } + if got := b.Closure(-2); got != 20 { + t.Fatalf("shared closure environment = %d", got) + } + original := a.Closure + t.Cleanup(func() { a.Closure = original }) + n := 35 + a.Closure = func(x int) int { return n + x } + if got := b.Closure(7); got != 42 { + t.Fatalf("captured closure = %d", got) + } + a.Closure = nil + if b.Closure != nil { + t.Fatal("nil closure not shared") + } + if &a.Slice != &b.Slice || !reflect.DeepEqual(a.Slice, []int{2, 3, 5}) { + t.Fatal("slice representation/initializer lost") + } +} + +//go:linkname linknameReserved go:llgo_test_reserved_storage +var linknameReserved int32 + +func TestLinknameGoNamespaceStorage(t *testing.T) { + if &linknameReserved != &a.GoNamespace || linknameReserved != 0 { + t.Fatal("go: symbol did not retain shared zero storage") + } + defer func() { linknameReserved = 0 }() + linknameReserved = 42 + if a.GoNamespace != 42 { + t.Fatal("go: symbol aliases do not share writes") + } +} diff --git a/test/go/testdata/linknamevars/a/vars.go b/test/go/testdata/linknamevars/a/vars.go new file mode 100644 index 0000000000..344d06aa8e --- /dev/null +++ b/test/go/testdata/linknamevars/a/vars.go @@ -0,0 +1,42 @@ +package a + +import _ "unsafe" + +//go:linkname Zero llgo_test_shared_zero +var Zero int + +//go:linkname Static llgo_test_shared_static +var Static int + +//go:linkname Dynamic llgo_test_shared_dynamic +var Dynamic = initialize() + +var Order []int + +func initialize() int { Order = append(Order, 1); return 11 } + +//go:linkname Closure llgo_test_shared_closure +var Closure func(int) int + +//go:linkname Slice llgo_test_shared_slice +var Slice []int + +//go:linkname Text llgo_test_shared_string +var Text string +var Copied = Text + +//go:linkname Marked +var Marked = 19 + +//go:linkname Named audit.linkname_qualified_var +var Named int + +//go:linkname Empty llgo_test_shared_empty +var Empty struct{} + +// This definition is deliberately unannotated. +var Default int +var DefaultCopied = Default + +//go:linkname GoNamespace go:llgo_test_reserved_storage +var GoNamespace int32 diff --git a/test/go/testdata/linknamevars/b/vars.go b/test/go/testdata/linknamevars/b/vars.go new file mode 100644 index 0000000000..ec509571cb --- /dev/null +++ b/test/go/testdata/linknamevars/b/vars.go @@ -0,0 +1,40 @@ +package b + +import ( + "github.com/xgo-dev/llgo/test/go/testdata/linknamevars/a" + _ "unsafe" +) + +//go:linkname Zero llgo_test_shared_zero +var Zero int + +//go:linkname Static llgo_test_shared_static +var Static = 41 + +//go:linkname Dynamic llgo_test_shared_dynamic +var Dynamic = initialize() + +func initialize() int { a.Order = append(a.Order, 2); return a.Dynamic + 7 } + +//go:linkname Closure llgo_test_shared_closure +var Closure = makeClosure(20) + +func makeClosure(n int) func(int) int { return func(v int) int { n += v; return n } } + +//go:linkname Slice llgo_test_shared_slice +var Slice = []int{2, 3, 5} + +//go:linkname Text llgo_test_shared_string +var Text = "linked value" + +//go:linkname Marked github.com/xgo-dev/llgo/test/go/testdata/linknamevars/a.Marked +var Marked int + +//go:linkname Named audit.linkname_qualified_var +var Named = 23 + +//go:linkname Empty llgo_test_shared_empty +var Empty struct{} + +//go:linkname Default github.com/xgo-dev/llgo/test/go/testdata/linknamevars/a.Default +var Default = 55 diff --git a/test/go/testdata/linknamevars/duplicate/a/var.go b/test/go/testdata/linknamevars/duplicate/a/var.go new file mode 100644 index 0000000000..487d87ca11 --- /dev/null +++ b/test/go/testdata/linknamevars/duplicate/a/var.go @@ -0,0 +1,6 @@ +package a + +import _ "unsafe" + +//go:linkname Value llgo_test_duplicate_data +var Value = 7 diff --git a/test/go/testdata/linknamevars/duplicate/b/var.go b/test/go/testdata/linknamevars/duplicate/b/var.go new file mode 100644 index 0000000000..cd9d4bc5c0 --- /dev/null +++ b/test/go/testdata/linknamevars/duplicate/b/var.go @@ -0,0 +1,6 @@ +package b + +import _ "unsafe" + +//go:linkname Value llgo_test_duplicate_data +var Value = 41 diff --git a/test/go/testdata/linknamevars/duplicate/main.go b/test/go/testdata/linknamevars/duplicate/main.go new file mode 100644 index 0000000000..b7f6c15604 --- /dev/null +++ b/test/go/testdata/linknamevars/duplicate/main.go @@ -0,0 +1,8 @@ +package main + +import ( + "github.com/xgo-dev/llgo/test/go/testdata/linknamevars/duplicate/a" + "github.com/xgo-dev/llgo/test/go/testdata/linknamevars/duplicate/b" +) + +func main() { println(a.Value, b.Value) }