diff --git a/internal/build/build.go b/internal/build/build.go index 2c7de4e4..73c7c9dd 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -782,11 +782,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa } }) // Generate main module file (needed for global variables even in library modes) - entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit) + entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit) + entryObjFile, err := exportObject(ctx, entryPkg.PkgPath, entryPkg.ExportFile, []byte(entryPkg.LPkg.String())) if err != nil { return err } - // defer os.Remove(entryLLFile) objFiles = append(objFiles, entryObjFile) // Compile extra files from target configuration @@ -908,118 +908,6 @@ func needStart(ctx *context) bool { } } -func genMainModuleFile(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) (path string, err error) { - var ( - pyInitDecl string - pyInit string - rtInitDecl string - rtInit string - ) - mainPkgPath := pkg.PkgPath - if needRuntime { - rtInit = "call void @\"" + rtPkgPath + ".init\"()" - rtInitDecl = "declare void @\"" + rtPkgPath + ".init\"()" - } - if needPyInit { - pyInit = "call void @Py_Initialize()" - pyInitDecl = "declare void @Py_Initialize()" - } - declSizeT := "%size_t = type i64" - if is32Bits(ctx.buildConf.Goarch) { - declSizeT = "%size_t = type i32" - } - stdioDecl := "" - stdioNobuf := "" - if IsStdioNobuf() { - stdioDecl = ` -@stdout = external global ptr -@stderr = external global ptr -@__stdout = external global ptr -@__stderr = external global ptr -declare i32 @setvbuf(ptr, ptr, i32, %size_t) - ` - stdioNobuf = ` -; Set stdout with no buffer -%stdout_is_null = icmp eq ptr @stdout, null -%stdout_ptr = select i1 %stdout_is_null, ptr @__stdout, ptr @stdout -call i32 @setvbuf(ptr %stdout_ptr, ptr null, i32 2, %size_t 0) -; Set stderr with no buffer -%stderr_ptr = select i1 %stdout_is_null, ptr @__stderr, ptr @stderr -call i32 @setvbuf(ptr %stderr_ptr, ptr null, i32 2, %size_t 0) - ` - } - // TODO(lijie): workaround for libc-free - // Remove main/_start when -buildmode and libc are ready - startDefine := ` -define weak void @_start() { - ; argc = 0 - %argc = add i32 0, 0 - ; argv = null - %argv = inttoptr i64 0 to i8** - call i32 @main(i32 %argc, i8** %argv) - ret void -} -` - mainDefine := "define i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr" - if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) { - mainDefine = "define hidden noundef i32 @__main_argc_argv(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr" - } - if !needStart(ctx) { - startDefine = "" - } - - var mainCode string - // For library modes (c-archive, c-shared), only generate global variables - if ctx.buildConf.BuildMode != BuildModeExe { - mainCode = `; ModuleID = 'main' -source_filename = "main" -@__llgo_argc = global i32 0, align 4 -@__llgo_argv = global ptr null, align 8 -` - } else { - // For executable mode, generate full main function - mainCode = fmt.Sprintf(`; ModuleID = 'main' -source_filename = "main" -%s -@__llgo_argc = global i32 0, align 4 -@__llgo_argv = global ptr null, align 8 -%s -%s -%s -declare void @"%s.init"() -declare void @"%s.main"() -define weak void @runtime.init() { - ret void -} - -; TODO(lijie): workaround for syscall patch -define weak void @"syscall.init"() { - ret void -} - -%s - -%s { -_llgo_0: - store i32 %%0, ptr @__llgo_argc, align 4 - store ptr %%1, ptr @__llgo_argv, align 8 - %s - %s - %s - call void @runtime.init() - call void @"%s.init"() - call void @"%s.main"() - ret i32 0 -} -`, declSizeT, stdioDecl, - pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath, - startDefine, mainDefine, stdioNobuf, - pyInit, rtInit, mainPkgPath, mainPkgPath) - } - - return exportObject(ctx, pkg.PkgPath+".main", pkg.ExportFile+"-main", []byte(mainCode)) -} - func is32Bits(goarch string) bool { return goarch == "386" || goarch == "arm" || goarch == "mips" || goarch == "wasm" } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c71f0d74..65aba541 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -159,3 +159,8 @@ func runBinary(t *testing.T, path string, args ...string) string { } return string(output) } + +func TestRunPrintfWithStdioNobuf(t *testing.T) { + t.Setenv(llgoStdioNobuf, "1") + mockRun([]string{"../../cl/_testdata/printf"}, &Config{Mode: ModeRun}) +} diff --git a/internal/build/main_module.go b/internal/build/main_module.go new file mode 100644 index 00000000..959debe4 --- /dev/null +++ b/internal/build/main_module.go @@ -0,0 +1,231 @@ +//go:build !llgo +// +build !llgo + +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). 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 build contains the llgo compiler build orchestration logic. +// +// The main_module.go file generates the entry point module for llgo programs, +// which contains the main() function, initialization sequence, and global +// variables like argc/argv. This module is generated differently depending on +// BuildMode (exe, c-archive, c-shared). + +package build + +import ( + "go/token" + "go/types" + + "github.com/goplus/llgo/internal/packages" + llvm "github.com/goplus/llvm" + + llssa "github.com/goplus/llgo/ssa" +) + +// genMainModule generates the main entry module for an llgo program. +// +// The module contains argc/argv globals and, for executable build modes, +// the entry function that wires initialization and main. For C archive or +// shared library modes, only the globals are emitted. +func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) Package { + prog := ctx.prog + mainPkg := prog.NewPackage("", pkg.ID+".main") + + argcVar := mainPkg.NewVarEx("__llgo_argc", prog.Pointer(prog.Int32())) + argcVar.Init(prog.Zero(prog.Int32())) + + argvValueType := prog.Pointer(prog.CStr()) + argvVar := mainPkg.NewVarEx("__llgo_argv", prog.Pointer(argvValueType)) + argvVar.InitNil() + + exportFile := pkg.ExportFile + if exportFile == "" { + exportFile = pkg.PkgPath + } + mainAPkg := &aPackage{ + Package: &packages.Package{ + PkgPath: pkg.PkgPath + ".main", + ExportFile: exportFile + "-main", + }, + LPkg: mainPkg, + } + + if ctx.buildConf.BuildMode != BuildModeExe { + return mainAPkg + } + + runtimeStub := defineWeakNoArgStub(mainPkg, "runtime.init") + // TODO(lijie): workaround for syscall patch + defineWeakNoArgStub(mainPkg, "syscall.init") + + var pyInit llssa.Function + if needPyInit { + pyInit = declareNoArgFunc(mainPkg, "Py_Initialize") + } + + var rtInit llssa.Function + if needRuntime { + rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") + } + + mainInit := declareNoArgFunc(mainPkg, pkg.PkgPath+".init") + mainMain := declareNoArgFunc(mainPkg, pkg.PkgPath+".main") + + entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, runtimeStub, mainInit, mainMain, pyInit, rtInit) + + if needStart(ctx) { + defineStart(mainPkg, entryFn, argvValueType) + } + + return mainAPkg +} + +// defineEntryFunction creates the program's entry function. The name is +// "main" for standard targets, or "__main_argc_argv" with hidden visibility +// for WASM targets that don't require _start. +// +// The entry stores argc/argv, optionally disables stdio buffering, runs +// initialization hooks (Python, runtime, package init), and finally calls +// main.main before returning 0. +func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa.Global, argvType llssa.Type, runtimeStub, mainInit, mainMain llssa.Function, pyInit, rtInit llssa.Function) llssa.Function { + prog := pkg.Prog + entryName := "main" + if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) { + entryName = "__main_argc_argv" + } + sig := newEntrySignature(argvType.RawType()) + fn := pkg.NewFunc(entryName, sig, llssa.InC) + fnVal := pkg.Module().NamedFunction(entryName) + if entryName != "main" { + fnVal.SetVisibility(llvm.HiddenVisibility) + fnVal.SetUnnamedAddr(true) + } + b := fn.MakeBody(1) + b.Store(argcVar.Expr, fn.Param(0)) + b.Store(argvVar.Expr, fn.Param(1)) + if IsStdioNobuf() { + emitStdioNobuf(b, pkg, ctx.buildConf.Goos) + } + if pyInit != nil { + b.Call(pyInit.Expr) + } + if rtInit != nil { + b.Call(rtInit.Expr) + } + b.Call(runtimeStub.Expr) + b.Call(mainInit.Expr) + b.Call(mainMain.Expr) + b.Return(prog.IntVal(0, prog.Int32())) + return fn +} + +func defineStart(pkg llssa.Package, entry llssa.Function, argvType llssa.Type) { + fn := pkg.NewFunc("_start", llssa.NoArgsNoRet, llssa.InC) + pkg.Module().NamedFunction("_start").SetLinkage(llvm.WeakAnyLinkage) + b := fn.MakeBody(1) + prog := pkg.Prog + b.Call(entry.Expr, prog.IntVal(0, prog.Int32()), prog.Nil(argvType)) + b.Return() +} + +func declareNoArgFunc(pkg llssa.Package, name string) llssa.Function { + return pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC) +} + +func defineWeakNoArgStub(pkg llssa.Package, name string) llssa.Function { + fn := pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC) + pkg.Module().NamedFunction(name).SetLinkage(llvm.WeakAnyLinkage) + b := fn.MakeBody(1) + b.Return() + return fn +} + +const ( + // ioNoBuf represents the _IONBF flag for setvbuf (no buffering) + ioNoBuf = 2 +) + +// emitStdioNobuf generates code to disable buffering on stdout and stderr +// when the LLGO_STDIO_NOBUF environment variable is set. Only Darwin uses +// the alternate `__stdoutp`/`__stderrp` symbols; other targets rely on the +// standard `stdout`/`stderr` globals. +func emitStdioNobuf(b llssa.Builder, pkg llssa.Package, goos string) { + prog := pkg.Prog + streamType := prog.VoidPtr() + streamPtrType := prog.Pointer(streamType) + + stdoutName := "stdout" + stderrName := "stderr" + if goos == "darwin" { + stdoutName = "__stdoutp" + stderrName = "__stderrp" + } + stdout := declareExternalPtrGlobal(pkg, stdoutName, streamPtrType) + stderr := declareExternalPtrGlobal(pkg, stderrName, streamPtrType) + stdoutPtr := b.Load(stdout) + stderrPtr := b.Load(stderr) + sizeType := prog.Uintptr() + setvbuf := declareSetvbuf(pkg, streamPtrType, prog.CStr(), prog.Int32(), sizeType) + + noBufMode := prog.IntVal(ioNoBuf, prog.Int32()) + zeroSize := prog.Zero(sizeType) + nullBuf := prog.Nil(prog.CStr()) + + b.Call(setvbuf.Expr, stdoutPtr, nullBuf, noBufMode, zeroSize) + b.Call(setvbuf.Expr, stderrPtr, nullBuf, noBufMode, zeroSize) +} + +func declareExternalPtrGlobal(pkg llssa.Package, name string, valueType llssa.Type) llssa.Expr { + global := pkg.NewVarEx(name, valueType) + pkg.Module().NamedGlobal(name).SetLinkage(llvm.ExternalLinkage) + return global.Expr +} + +func declareSetvbuf(pkg llssa.Package, streamPtrType, bufPtrType, intType, sizeType llssa.Type) llssa.Function { + sig := newSignature( + []types.Type{ + streamPtrType.RawType(), + bufPtrType.RawType(), + intType.RawType(), + sizeType.RawType(), + }, + []types.Type{intType.RawType()}, + ) + return pkg.NewFunc("setvbuf", sig, llssa.InC) +} + +func tupleOf(tys ...types.Type) *types.Tuple { + if len(tys) == 0 { + return types.NewTuple() + } + vars := make([]*types.Var, len(tys)) + for i, t := range tys { + vars[i] = types.NewParam(token.NoPos, nil, "", t) + } + return types.NewTuple(vars...) +} + +func newSignature(params []types.Type, results []types.Type) *types.Signature { + return types.NewSignatureType(nil, nil, nil, tupleOf(params...), tupleOf(results...), false) +} + +func newEntrySignature(argvType types.Type) *types.Signature { + return newSignature( + []types.Type{types.Typ[types.Int32], argvType}, + []types.Type{types.Typ[types.Int32]}, + ) +} diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go new file mode 100644 index 00000000..96f87ac6 --- /dev/null +++ b/internal/build/main_module_test.go @@ -0,0 +1,70 @@ +//go:build !llgo +// +build !llgo + +package build + +import ( + "strings" + "testing" + + "github.com/goplus/llvm" + + "github.com/goplus/llgo/internal/packages" + llssa "github.com/goplus/llgo/ssa" +) + +func init() { + llssa.Initialize(llssa.InitAll) +} + +func TestGenMainModuleExecutable(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + ctx := &context{ + prog: llssa.NewProgram(nil), + buildConf: &Config{ + BuildMode: BuildModeExe, + Goos: "linux", + Goarch: "amd64", + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + mod := genMainModule(ctx, llssa.PkgRuntime, pkg, true, true) + if mod.ExportFile != "foo.a-main" { + t.Fatalf("unexpected export file: %s", mod.ExportFile) + } + ir := mod.LPkg.String() + checks := []string{ + "define i32 @main(", + "call void @Py_Initialize()", + "call void @\"example.com/foo.init\"()", + "define weak void @_start()", + } + for _, want := range checks { + if !strings.Contains(ir, want) { + t.Fatalf("main module IR missing %q:\n%s", want, ir) + } + } +} + +func TestGenMainModuleLibrary(t *testing.T) { + llvm.InitializeAllTargets() + t.Setenv(llgoStdioNobuf, "") + ctx := &context{ + prog: llssa.NewProgram(nil), + buildConf: &Config{ + BuildMode: BuildModeCArchive, + Goos: "linux", + Goarch: "amd64", + }, + } + pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"} + mod := genMainModule(ctx, llssa.PkgRuntime, pkg, false, false) + ir := mod.LPkg.String() + if strings.Contains(ir, "define i32 @main") { + t.Fatalf("library mode should not emit main function:\n%s", ir) + } + if !strings.Contains(ir, "@__llgo_argc = global i32 0") { + t.Fatalf("library mode missing argc global:\n%s", ir) + } +}