From 869e917dc6959445fac6ee4d93cd10c98ab445e5 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 5 Nov 2021 13:23:08 +0100 Subject: [PATCH] all: add support for windows/amd64 This uses Mingw-w64, which seems to be the de facto standard for porting Unixy programs to Windows. --- .github/workflows/windows.yml | 2 + .gitmodules | 3 + Makefile | 12 +- builder/ar.go | 70 ++++-- builder/build.go | 9 + builder/builder_test.go | 1 + builder/lld.cpp | 5 + builder/mingw-w64.go | 91 +++++++ builder/tools-builtin.go | 14 +- compileopts/config.go | 9 + compileopts/target.go | 34 ++- compiler/syscall.go | 63 ++++- lib/mingw-w64 | 1 + main_test.go | 25 +- src/crypto/rand/rand_windows.go | 42 ++++ src/internal/task/task_stack_amd64.go | 2 +- src/internal/task/task_stack_amd64_windows.S | 57 +++++ src/internal/task/task_stack_amd64_windows.go | 66 +++++ src/os/file_anyos.go | 114 +++++++++ src/os/file_unix.go | 111 +-------- src/os/file_windows.go | 7 + src/runtime/gc_amd64_windows.S | 22 ++ src/runtime/os_windows.go | 3 + src/runtime/runtime_windows.go | 230 ++++++++++++++++++ src/syscall/syscall_libc_darwin.go | 6 + testdata/filesystem.go | 3 +- 26 files changed, 841 insertions(+), 161 deletions(-) create mode 100644 builder/mingw-w64.go create mode 160000 lib/mingw-w64 create mode 100644 src/crypto/rand/rand_windows.go create mode 100644 src/internal/task/task_stack_amd64_windows.S create mode 100644 src/internal/task/task_stack_amd64_windows.go create mode 100644 src/os/file_anyos.go create mode 100644 src/os/file_windows.go create mode 100644 src/runtime/gc_amd64_windows.S create mode 100644 src/runtime/os_windows.go create mode 100644 src/runtime/runtime_windows.go diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 49cd86e9..b4014128 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -99,3 +99,5 @@ jobs: - name: Smoke tests shell: bash run: make smoketest TINYGO=build/tinygo AVR=0 XTENSA=0 + - name: Test stdlib packages + run: make tinygo-test diff --git a/.gitmodules b/.gitmodules index c126bd4b..8143ce84 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ [submodule "lib/binaryen"] path = lib/binaryen url = https://github.com/WebAssembly/binaryen.git +[submodule "lib/mingw-w64"] + path = lib/mingw-w64 + url = https://github.com/mingw-w64/mingw-w64.git diff --git a/Makefile b/Makefile index 0c40b937..72238dd1 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ endif .PHONY: all tinygo test $(LLVM_BUILDDIR) llvm-source clean fmt gen-device gen-device-nrf gen-device-nxp gen-device-avr gen-device-rp -LLVM_COMPONENTS = all-targets analysis asmparser asmprinter bitreader bitwriter codegen core coroutines coverage debuginfodwarf executionengine frontendopenmp instrumentation interpreter ipo irreader linker lto mc mcjit objcarcopts option profiledata scalaropts support target +LLVM_COMPONENTS = all-targets analysis asmparser asmprinter bitreader bitwriter codegen core coroutines coverage debuginfodwarf debuginfopdb executionengine frontendopenmp instrumentation interpreter ipo irreader libdriver linker lto mc mcjit objcarcopts option profiledata scalaropts support target windowsmanifest ifeq ($(OS),Windows_NT) EXE = .exe @@ -477,7 +477,10 @@ endif $(TINYGO) build -size short -o test.hex -target=pca10040 -opt=0 ./testdata/stdlib.go @$(MD5SUM) test.hex GOOS=linux GOARCH=arm $(TINYGO) build -size short -o test.elf ./testdata/cgo + GOOS=windows GOARCH=amd64 $(TINYGO) build -o test.exe ./testdata/cgo ifneq ($(OS),Windows_NT) + # TODO: this does not yet work on Windows. Somehow, unused functions are + # not garbage collected. $(TINYGO) build -o test.elf -gc=leaking -scheduler=none examples/serial endif @@ -490,6 +493,8 @@ build/release: tinygo gen-device wasi-libc binaryen @mkdir -p build/release/tinygo/lib/clang/include @mkdir -p build/release/tinygo/lib/CMSIS/CMSIS @mkdir -p build/release/tinygo/lib/compiler-rt/lib + @mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common + @mkdir -p build/release/tinygo/lib/mingw-w64/mingw-w64-headers/defaults @mkdir -p build/release/tinygo/lib/musl/arch @mkdir -p build/release/tinygo/lib/musl/crt @mkdir -p build/release/tinygo/lib/musl/src @@ -530,6 +535,11 @@ build/release: tinygo gen-device wasi-libc binaryen @cp -rp lib/musl/src/thread build/release/tinygo/lib/musl/src @cp -rp lib/musl/src/time build/release/tinygo/lib/musl/src @cp -rp lib/musl/src/unistd build/release/tinygo/lib/musl/src + @cp -rp lib/mingw-w64/mingw-w64-crt/def-include build/release/tinygo/lib/mingw-w64/mingw-w64-crt + @cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/api-ms-win-crt-* build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common + @cp -rp lib/mingw-w64/mingw-w64-crt/lib-common/kernel32.def.in build/release/tinygo/lib/mingw-w64/mingw-w64-crt/lib-common + @cp -rp lib/mingw-w64/mingw-w64-headers/crt/ build/release/tinygo/lib/mingw-w64/mingw-w64-headers + @cp -rp lib/mingw-w64/mingw-w64-headers/defaults/include build/release/tinygo/lib/mingw-w64/mingw-w64-headers/defaults @cp -rp lib/nrfx/* build/release/tinygo/lib/nrfx @cp -rp lib/picolibc/newlib/libc/ctype build/release/tinygo/lib/picolibc/newlib/libc @cp -rp lib/picolibc/newlib/libc/include build/release/tinygo/lib/picolibc/newlib/libc diff --git a/builder/ar.go b/builder/ar.go index dcdc48d4..7e33e731 100644 --- a/builder/ar.go +++ b/builder/ar.go @@ -3,6 +3,7 @@ package builder import ( "bytes" "debug/elf" + "debug/pe" "encoding/binary" "errors" "fmt" @@ -39,29 +40,42 @@ func makeArchive(arfile *os.File, objs []string) error { } // Read the symbols and add them to the symbol table. - dbg, err := elf.NewFile(objfile) - if err != nil { - return fmt.Errorf("failed to open file %s: %w", objpath, err) - } - symbols, err := dbg.Symbols() - if err != nil { - return err - } - for _, symbol := range symbols { - bind := elf.ST_BIND(symbol.Info) - if bind != elf.STB_GLOBAL && bind != elf.STB_WEAK { - // Don't include local symbols (STB_LOCAL). - continue + if dbg, err := elf.NewFile(objfile); err == nil { + symbols, err := dbg.Symbols() + if err != nil { + return err } - if elf.ST_TYPE(symbol.Info) != elf.STT_FUNC && elf.ST_TYPE(symbol.Info) != elf.STT_OBJECT { - // Not a function. - continue + for _, symbol := range symbols { + bind := elf.ST_BIND(symbol.Info) + if bind != elf.STB_GLOBAL && bind != elf.STB_WEAK { + // Don't include local symbols (STB_LOCAL). + continue + } + if elf.ST_TYPE(symbol.Info) != elf.STT_FUNC && elf.ST_TYPE(symbol.Info) != elf.STT_OBJECT { + // Not a function. + continue + } + // Include in archive. + symbolTable = append(symbolTable, struct { + name string + fileIndex int + }{symbol.Name, i}) } - // Include in archive. - symbolTable = append(symbolTable, struct { - name string - fileIndex int - }{symbol.Name, i}) + } else if dbg, err := pe.NewFile(objfile); err == nil { + for _, symbol := range dbg.Symbols { + if symbol.StorageClass != 2 { + continue + } + if symbol.SectionNumber == 0 { + continue + } + symbolTable = append(symbolTable, struct { + name string + fileIndex int + }{symbol.Name, i}) + } + } else { + return fmt.Errorf("failed to open file %s as ELF or PE/COFF: %w", objpath, err) } // Close file, to avoid issues with too many open files (especially on @@ -120,11 +134,13 @@ func makeArchive(arfile *os.File, objs []string) error { } // Add all object files to the archive. + var copyBuf bytes.Buffer for i, objpath := range objs { objfile, err := os.Open(objpath) if err != nil { return err } + defer objfile.Close() // Store the start index, for when we'll update the symbol table with // the correct file start indices. @@ -155,13 +171,21 @@ func makeArchive(arfile *os.File, objs []string) error { } // Copy the file contents into the archive. - n, err := io.Copy(arwriter, objfile) + // First load all contents into a buffer, then write it all in one go to + // the archive file. This is a bit complicated, but is necessary because + // io.Copy can't deal with files that are of an odd size. + copyBuf.Reset() + n, err := io.Copy(©Buf, objfile) if err != nil { - return err + return fmt.Errorf("could not copy object file into ar file: %w", err) } if n != st.Size() { return errors.New("file modified during ar creation: " + arfile.Name()) } + _, err = arwriter.Write(copyBuf.Bytes()) + if err != nil { + return fmt.Errorf("could not copy object file into ar file: %w", err) + } // File is not needed anymore. objfile.Close() diff --git a/builder/build.go b/builder/build.go index 25271496..3bb685a8 100644 --- a/builder/build.go +++ b/builder/build.go @@ -115,6 +115,12 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil return errors.New("could not find wasi-libc, perhaps you need to run `make wasi-libc`?") } libcDependencies = append(libcDependencies, dummyCompileJob(path)) + case "mingw-w64": + _, err := MinGW.load(config, dir) + if err != nil { + return err + } + libcDependencies = append(libcDependencies, makeMinGWExtraLibs(dir)...) case "": // no library specified, so nothing to do default: @@ -518,6 +524,9 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil // Prepare link command. linkerDependencies := []*compileJob{outputObjectFileJob} executable := filepath.Join(dir, "main") + if config.GOOS() == "windows" { + executable += ".exe" + } tmppath := executable // final file ldflags := append(config.LDFlags(), "-o", executable) diff --git a/builder/builder_test.go b/builder/builder_test.go index 6e08b9ac..cff43db8 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -58,6 +58,7 @@ func TestClangAttributes(t *testing.T) { {GOOS: "linux", GOARCH: "arm64"}, {GOOS: "darwin", GOARCH: "amd64"}, {GOOS: "darwin", GOARCH: "arm64"}, + {GOOS: "windows", GOARCH: "amd64"}, } { t.Run("GOOS="+options.GOOS+",GOARCH="+options.GOARCH, func(t *testing.T) { testClangAttributes(t, options) diff --git a/builder/lld.cpp b/builder/lld.cpp index 9231df9c..5a9ad8a2 100644 --- a/builder/lld.cpp +++ b/builder/lld.cpp @@ -11,6 +11,11 @@ bool tinygo_link_elf(int argc, char **argv) { return lld::elf::link(args, false, llvm::outs(), llvm::errs()); } +bool tinygo_link_mingw(int argc, char **argv) { + std::vector args(argv, argv + argc); + return lld::mingw::link(args, false, llvm::outs(), llvm::errs()); +} + bool tinygo_link_wasm(int argc, char **argv) { std::vector args(argv, argv + argc); return lld::wasm::link(args, false, llvm::outs(), llvm::errs()); diff --git a/builder/mingw-w64.go b/builder/mingw-w64.go new file mode 100644 index 00000000..bdab730f --- /dev/null +++ b/builder/mingw-w64.go @@ -0,0 +1,91 @@ +package builder + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/tinygo-org/tinygo/goenv" +) + +var MinGW = Library{ + name: "mingw-w64", + makeHeaders: func(target, includeDir string) error { + // copy _mingw.h + srcDir := filepath.Join(goenv.Get("TINYGOROOT"), "lib", "mingw-w64") + outf, err := os.Create(includeDir + "/_mingw.h") + if err != nil { + return err + } + defer outf.Close() + inf, err := os.Open(srcDir + "/mingw-w64-headers/crt/_mingw.h.in") + if err != nil { + return err + } + _, err = io.Copy(outf, inf) + return err + }, + cflags: func(target, headerPath string) []string { + // No flags necessary because there are no files to compile. + return nil + }, + librarySources: func(target string) []string { + // We only use the UCRT DLL file. No source files necessary. + return nil + }, +} + +// makeMinGWExtraLibs returns a slice of jobs to import the correct .dll +// libraries. This is done by converting input .def files to .lib files which +// can then be linked as usual. +// +// TODO: cache the result. At the moment, it costs a few hundred milliseconds to +// compile these files. +func makeMinGWExtraLibs(tmpdir string) []*compileJob { + var jobs []*compileJob + root := goenv.Get("TINYGOROOT") + // Normally all the api-ms-win-crt-*.def files are all compiled to a single + // .lib file. But to simplify things, we're going to leave them as separate + // files. + for _, name := range []string{ + "kernel32.def.in", + "api-ms-win-crt-conio-l1-1-0.def", + "api-ms-win-crt-convert-l1-1-0.def", + "api-ms-win-crt-environment-l1-1-0.def", + "api-ms-win-crt-filesystem-l1-1-0.def", + "api-ms-win-crt-heap-l1-1-0.def", + "api-ms-win-crt-locale-l1-1-0.def", + "api-ms-win-crt-math-l1-1-0.def.in", + "api-ms-win-crt-multibyte-l1-1-0.def", + "api-ms-win-crt-private-l1-1-0.def.in", + "api-ms-win-crt-process-l1-1-0.def", + "api-ms-win-crt-runtime-l1-1-0.def.in", + "api-ms-win-crt-stdio-l1-1-0.def", + "api-ms-win-crt-string-l1-1-0.def", + "api-ms-win-crt-time-l1-1-0.def", + "api-ms-win-crt-utility-l1-1-0.def", + } { + outpath := filepath.Join(tmpdir, filepath.Base(name)+".lib") + inpath := filepath.Join(root, "lib/mingw-w64/mingw-w64-crt/lib-common/"+name) + job := &compileJob{ + description: "create lib file " + inpath, + result: outpath, + run: func(job *compileJob) error { + defpath := inpath + if strings.HasSuffix(inpath, ".in") { + // .in files need to be preprocessed by a preprocessor (-E) + // first. + defpath = outpath + ".def" + err := runCCompiler("-E", "-x", "c", "-Wp,-w", "-P", "-DDEF_X64", "-o", defpath, inpath, "-I"+goenv.Get("TINYGOROOT")+"/lib/mingw-w64/mingw-w64-crt/def-include/") + if err != nil { + return err + } + } + return link("ld.lld", "-m", "i386pep", "-o", outpath, defpath) + }, + } + jobs = append(jobs, job) + } + return jobs +} diff --git a/builder/tools-builtin.go b/builder/tools-builtin.go index 61e7a325..29dfa861 100644 --- a/builder/tools-builtin.go +++ b/builder/tools-builtin.go @@ -13,6 +13,7 @@ import ( #include bool tinygo_clang_driver(int argc, char **argv); bool tinygo_link_elf(int argc, char **argv); +bool tinygo_link_mingw(int argc, char **argv); bool tinygo_link_wasm(int argc, char **argv); */ import "C" @@ -24,6 +25,10 @@ const hasBuiltinTools = true // This version actually runs the tools because TinyGo was compiled while // linking statically with LLVM (with the byollvm build tag). func RunTool(tool string, args ...string) error { + linker := "elf" + if tool == "ld.lld" && len(args) >= 2 && args[0] == "-m" && args[1] == "i386pep" { + linker = "mingw" + } args = append([]string{"tinygo:" + tool}, args...) var cflag *C.char @@ -41,7 +46,14 @@ func RunTool(tool string, args ...string) error { case "clang": ok = C.tinygo_clang_driver(C.int(len(args)), (**C.char)(buf)) case "ld.lld": - ok = C.tinygo_link_elf(C.int(len(args)), (**C.char)(buf)) + switch linker { + case "elf": + ok = C.tinygo_link_elf(C.int(len(args)), (**C.char)(buf)) + case "mingw": + ok = C.tinygo_link_mingw(C.int(len(args)), (**C.char)(buf)) + default: + return errors.New("unknown linker: " + linker) + } case "wasm-ld": ok = C.tinygo_link_wasm(C.int(len(args)), (**C.char)(buf)) default: diff --git a/compileopts/config.go b/compileopts/config.go index 99c43c6f..22cb676e 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -272,6 +272,15 @@ func (c *Config) CFlags() []string { case "wasi-libc": root := goenv.Get("TINYGOROOT") cflags = append(cflags, "--sysroot="+root+"/lib/wasi-libc/sysroot") + case "mingw-w64": + root := goenv.Get("TINYGOROOT") + path, _ := c.LibcPath("mingw-w64") + cflags = append(cflags, + "--sysroot="+path, + "-Xclang", "-internal-isystem", "-Xclang", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "crt"), + "-Xclang", "-internal-isystem", "-Xclang", filepath.Join(root, "lib", "mingw-w64", "mingw-w64-headers", "defaults", "include"), + "-D_UCRT", + ) case "": // No libc specified, nothing to add. default: diff --git a/compileopts/target.go b/compileopts/target.go index 5440536d..2e5f5487 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -206,6 +206,9 @@ func LoadTarget(options *Options) (*TargetSpec, error) { if options.GOARCH == "arm" { target += "-gnueabihf" } + if options.GOOS == "windows" { + target += "-gnu" + } return defaultTarget(options.GOOS, options.GOARCH, target) } @@ -230,13 +233,7 @@ func LoadTarget(options *Options) (*TargetSpec, error) { return spec, nil } -// WindowsBuildNotSupportedErr is being thrown, when goos is windows and no target has been specified. -var WindowsBuildNotSupportedErr = errors.New("Building Windows binaries is currently not supported. Try specifying a different target") - func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) { - if goos == "windows" { - return nil, WindowsBuildNotSupportedErr - } // No target spec available. Use the default one, useful on most systems // with a regular OS. spec := TargetSpec{ @@ -279,12 +276,28 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) { spec.RTLib = "compiler-rt" spec.Libc = "musl" spec.LDFlags = append(spec.LDFlags, "--gc-sections") + } else if goos == "windows" { + spec.Linker = "ld.lld" + spec.Libc = "mingw-w64" + spec.LDFlags = append(spec.LDFlags, + "-m", "i386pep", + "-Bdynamic", + "--image-base", "0x400000", + "--gc-sections", + "--no-insert-timestamp", + ) } else { spec.LDFlags = append(spec.LDFlags, "-no-pie", "-Wl,--gc-sections") // WARNING: clang < 5.0 requires -nopie } if goarch != "wasm" { - spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/gc_"+goarch+".S") - spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+goarch+".S") + suffix := "" + if goos == "windows" { + // Windows uses a different calling convention from other operating + // systems so we need separate assembly files. + suffix = "_windows" + } + spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/gc_"+goarch+suffix+".S") + spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+goarch+suffix+".S") } if goarch != runtime.GOARCH { // Some educated guesses as to how to invoke helper programs. @@ -296,6 +309,11 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) { spec.Emulator = []string{"qemu-aarch64"} } } + if goos != runtime.GOOS { + if goos == "windows" { + spec.Emulator = []string{"wine"} + } + } return &spec, nil } diff --git a/compiler/syscall.go b/compiler/syscall.go index 6a0bd328..db379d52 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -156,12 +156,12 @@ func (b *builder) createRawSyscall(call *ssa.CallCommon) (llvm.Value, error) { // createSyscall emits instructions for the syscall.Syscall* family of // functions, depending on the target OS/arch. func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { - syscallResult, err := b.createRawSyscall(call) - if err != nil { - return syscallResult, err - } switch b.GOOS { case "linux", "freebsd": + syscallResult, err := b.createRawSyscall(call) + if err != nil { + return syscallResult, err + } // Return values: r0, r1 uintptr, err Errno // Pseudocode: // var err uintptr @@ -180,6 +180,10 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { retval = b.CreateInsertValue(retval, errResult, 2, "") return retval, nil case "darwin": + syscallResult, err := b.createRawSyscall(call) + if err != nil { + return syscallResult, err + } // Return values: r0, r1 uintptr, err Errno // Pseudocode: // var err uintptr @@ -195,6 +199,57 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { retval = b.CreateInsertValue(retval, zero, 1, "") retval = b.CreateInsertValue(retval, errResult, 2, "") return retval, nil + case "windows": + // On Windows, syscall.Syscall* is basically just a function pointer + // call. This is complicated in gc because of stack switching and the + // different ABI, but easy in TinyGo: just call the function pointer. + // The signature looks like this: + // func Syscall(trap, nargs, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) + + // Prepare input values. + var paramTypes []llvm.Type + var params []llvm.Value + for _, val := range call.Args[2:] { + param := b.getValue(val) + params = append(params, param) + paramTypes = append(paramTypes, param.Type()) + } + llvmType := llvm.FunctionType(b.uintptrType, paramTypes, false) + fn := b.getValue(call.Args[0]) + fnPtr := b.CreateIntToPtr(fn, llvm.PointerType(llvmType, 0), "") + + // Prepare some functions that will be called later. + setLastError := b.mod.NamedFunction("SetLastError") + if setLastError.IsNil() { + llvmType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.ctx.Int32Type()}, false) + setLastError = llvm.AddFunction(b.mod, "SetLastError", llvmType) + } + getLastError := b.mod.NamedFunction("GetLastError") + if getLastError.IsNil() { + llvmType := llvm.FunctionType(b.ctx.Int32Type(), nil, false) + getLastError = llvm.AddFunction(b.mod, "GetLastError", llvmType) + } + + // Now do the actual call. Pseudocode: + // SetLastError(0) + // r1 = trap(a1, a2, a3, ...) + // err = uintptr(GetLastError()) + // return r1, 0, err + // Note that SetLastError/GetLastError could be replaced with direct + // access to the thread control block, which is probably smaller and + // faster. The Go runtime does this in assembly. + b.CreateCall(setLastError, []llvm.Value{llvm.ConstNull(b.ctx.Int32Type())}, "") + syscallResult := b.CreateCall(fnPtr, params, "") + errResult := b.CreateCall(getLastError, nil, "err") + if b.uintptrType != b.ctx.Int32Type() { + errResult = b.CreateZExt(errResult, b.uintptrType, "err.uintptr") + } + + // Return r1, 0, err + retval := llvm.ConstNull(b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.uintptrType}, false)) + retval = b.CreateInsertValue(retval, syscallResult, 0, "") + retval = b.CreateInsertValue(retval, errResult, 2, "") + return retval, nil default: return llvm.Value{}, b.makeError(call.Pos(), "unknown GOOS/GOARCH for syscall: "+b.GOOS+"/"+b.GOARCH) } diff --git a/lib/mingw-w64 b/lib/mingw-w64 new file mode 160000 index 00000000..acc9b9d9 --- /dev/null +++ b/lib/mingw-w64 @@ -0,0 +1 @@ +Subproject commit acc9b9d9eb63a13d8122cbac4882eb5f4ee2f679 diff --git a/main_test.go b/main_test.go index 8fd286be..557ac2bf 100644 --- a/main_test.go +++ b/main_test.go @@ -71,11 +71,9 @@ func TestCompiler(t *testing.T) { return } - if runtime.GOOS != "windows" { - t.Run("Host", func(t *testing.T) { - runPlatTests(optionsFromTarget(""), tests, t) - }) - } + t.Run("Host", func(t *testing.T) { + runPlatTests(optionsFromTarget(""), tests, t) + }) if testing.Short() { return @@ -113,10 +111,6 @@ func TestCompiler(t *testing.T) { // Test a few build options. t.Run("build-options", func(t *testing.T) { - if runtime.GOOS == "windows" { - // These tests assume a host that is supported by TinyGo. - t.Skip("can't test build options on Windows") - } t.Parallel() // Test with few optimizations enabled (no inlining, etc). @@ -298,6 +292,9 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c // Build the test binary. binary := filepath.Join(tmpdir, "test") + if spec.GOOS == "windows" { + binary += ".exe" + } err = runBuild("./"+path, binary, &options) if err != nil { printCompilerError(t.Log, err) @@ -339,7 +336,13 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c } go func() { // Terminate the process if it runs too long. - timer := time.NewTimer(10 * time.Second) + maxDuration := 10 * time.Second + if runtime.GOOS == "windows" { + // For some reason, tests on Windows can take around + // 30s to complete. TODO: investigate why and fix this. + maxDuration = 40 * time.Second + } + timer := time.NewTimer(maxDuration) select { case <-runComplete: timer.Stop() @@ -369,7 +372,7 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c t.Log("failed to run:", err) fail = true } else if !bytes.Equal(expected, actual) { - t.Log("output did not match") + t.Logf("output did not match (expected %d bytes, got %d bytes):", len(expected), len(actual)) fail = true } diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go new file mode 100644 index 00000000..9a840c32 --- /dev/null +++ b/src/crypto/rand/rand_windows.go @@ -0,0 +1,42 @@ +package rand + +import "errors" + +func init() { + Reader = &reader{} +} + +type reader struct { +} + +var errRandom = errors.New("failed to obtain random data from rand_s") + +func (r *reader) Read(b []byte) (n int, err error) { + if len(b) == 0 { + return + } + + var randomByte uint32 + for i := range b { + // Call rand_s every four bytes because it's a C int (always 32-bit in + // Windows). + if i%4 == 0 { + errCode := libc_rand_s(&randomByte) + if errCode != 0 { + // According to the documentation, it can return an error. + return n, errRandom + } + } else { + randomByte >>= 8 + } + b[i] = byte(randomByte) + } + + return len(b), nil +} + +// Cryptographically secure random number generator. +// https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/rand-s?view=msvc-170 +// errno_t rand_s(unsigned int* randomValue); +//export rand_s +func libc_rand_s(randomValue *uint32) int32 diff --git a/src/internal/task/task_stack_amd64.go b/src/internal/task/task_stack_amd64.go index 7f1d132e..f8d55781 100644 --- a/src/internal/task/task_stack_amd64.go +++ b/src/internal/task/task_stack_amd64.go @@ -1,4 +1,4 @@ -// +build scheduler.tasks,amd64 +// +build scheduler.tasks,amd64,!windows package task diff --git a/src/internal/task/task_stack_amd64_windows.S b/src/internal/task/task_stack_amd64_windows.S new file mode 100644 index 00000000..bdbcfca6 --- /dev/null +++ b/src/internal/task/task_stack_amd64_windows.S @@ -0,0 +1,57 @@ +// Windows on amd64 has a slightly different ABI than other (*nix) systems. +// Therefore, assembly functions need to be tweaked slightly. + +.section .text.tinygo_startTask,"ax" +.global tinygo_startTask +tinygo_startTask: + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r12 contain the pc of the to-be-started function and + // r13 contain the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + movq %r13, %rcx + + // Branch to the "goroutine start" function. + callq *%r12 + + // After return, exit this goroutine. This is a tail call. + jmp tinygo_pause + +.global tinygo_swapTask +.section .text.tinygo_swapTask,"ax" +tinygo_swapTask: + // This function gets the following parameters: + // %rcx = newStack uintptr + // %rdx = oldStack *uintptr + + // Save all callee-saved registers: + pushq %r15 + pushq %r14 + pushq %r13 + pushq %r12 + pushq %rsi + pushq %rdi + pushq %rbp + pushq %rbx + + // Save the current stack pointer in oldStack. + movq %rsp, (%rdx) + + // Switch to the new stack pointer. + movq %rcx, %rsp + + // Load saved register from the new stack. + popq %rbx + popq %rbp + popq %rdi + popq %rsi + popq %r12 + popq %r13 + popq %r14 + popq %r15 + + // Return into the new task, as if tinygo_swapTask was a regular call. + ret diff --git a/src/internal/task/task_stack_amd64_windows.go b/src/internal/task/task_stack_amd64_windows.go new file mode 100644 index 00000000..e5ee47eb --- /dev/null +++ b/src/internal/task/task_stack_amd64_windows.go @@ -0,0 +1,66 @@ +// +build scheduler.tasks,amd64,windows + +package task + +// This is almost the same as task_stack_amd64.go, but with the extra rdi and +// rsi registers saved: Windows has a slightly different calling convention. + +import "unsafe" + +var systemStack uintptr + +// calleeSavedRegs is the list of registers that must be saved and restored when +// switching between tasks. Also see task_stack_amd64_windows.S that relies on +// the exact layout of this struct. +type calleeSavedRegs struct { + rbx uintptr + rbp uintptr + rdi uintptr + rsi uintptr + r12 uintptr + r13 uintptr + r14 uintptr + r15 uintptr + + pc uintptr +} + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(r)) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + + // Start the function at tinygo_startTask (defined in + // src/internal/task/task_stack_amd64_windows.S). This assembly code calls a + // function (passed in r12) with a single argument (passed in r13). After + // the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in r12. + // This function is a compiler-generated wrapper which loads arguments out + // of a struct pointer. See createGoroutineStartWrapper (defined in + // compiler/goroutine.go) for more information. + r.r12 = fn + + // Pass the pointer to the arguments struct in r13. + r.r13 = uintptr(args) +} + +func (s *state) resume() { + swapTask(s.sp, &systemStack) +} + +func (s *state) pause() { + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.sp) +} + +// SystemStack returns the system stack pointer when called from a task stack. +// When called from the system stack, it returns 0. +func SystemStack() uintptr { + return systemStack +} diff --git a/src/os/file_anyos.go b/src/os/file_anyos.go new file mode 100644 index 00000000..cd938b6b --- /dev/null +++ b/src/os/file_anyos.go @@ -0,0 +1,114 @@ +// +build !baremetal,!js + +package os + +import ( + "io" + "syscall" +) + +func init() { + // Mount the host filesystem at the root directory. This is what most + // programs will be expecting. + Mount("/", unixFilesystem{}) +} + +// Stdin, Stdout, and Stderr are open Files pointing to the standard input, +// standard output, and standard error file descriptors. +var ( + Stdin = &File{unixFileHandle(syscall.Stdin), "/dev/stdin"} + Stdout = &File{unixFileHandle(syscall.Stdout), "/dev/stdout"} + Stderr = &File{unixFileHandle(syscall.Stderr), "/dev/stderr"} +) + +// isOS indicates whether we're running on a real operating system with +// filesystem support. +const isOS = true + +// unixFilesystem is an empty handle for a Unix/Linux filesystem. All operations +// are relative to the current working directory. +type unixFilesystem struct { +} + +func (fs unixFilesystem) Mkdir(path string, perm FileMode) error { + return handleSyscallError(syscall.Mkdir(path, uint32(perm))) +} + +func (fs unixFilesystem) Remove(path string) error { + return handleSyscallError(syscall.Unlink(path)) +} + +func (fs unixFilesystem) OpenFile(path string, flag int, perm FileMode) (FileHandle, error) { + // Map os package flags to syscall flags. + syscallFlag := 0 + if flag&O_RDONLY != 0 { + syscallFlag |= syscall.O_RDONLY + } + if flag&O_WRONLY != 0 { + syscallFlag |= syscall.O_WRONLY + } + if flag&O_RDWR != 0 { + syscallFlag |= syscall.O_RDWR + } + if flag&O_APPEND != 0 { + syscallFlag |= syscall.O_APPEND + } + if flag&O_CREATE != 0 { + syscallFlag |= syscall.O_CREAT + } + if flag&O_EXCL != 0 { + syscallFlag |= syscall.O_EXCL + } + if flag&O_SYNC != 0 { + syscallFlag |= syscall.O_SYNC + } + if flag&O_TRUNC != 0 { + syscallFlag |= syscall.O_TRUNC + } + fp, err := syscall.Open(path, syscallFlag, uint32(perm)) + return unixFileHandle(fp), handleSyscallError(err) +} + +// unixFileHandle is a Unix file pointer with associated methods that implement +// the FileHandle interface. +type unixFileHandle uintptr + +// Read reads up to len(b) bytes from the File. It returns the number of bytes +// read and any error encountered. At end of file, Read returns 0, io.EOF. +func (f unixFileHandle) Read(b []byte) (n int, err error) { + n, err = syscall.Read(syscallFd(f), b) + err = handleSyscallError(err) + if n == 0 && err == nil { + err = io.EOF + } + return +} + +// Write writes len(b) bytes to the File. It returns the number of bytes written +// and an error, if any. Write returns a non-nil error when n != len(b). +func (f unixFileHandle) Write(b []byte) (n int, err error) { + n, err = syscall.Write(syscallFd(f), b) + err = handleSyscallError(err) + return +} + +// Close closes the File, rendering it unusable for I/O. +func (f unixFileHandle) Close() error { + return handleSyscallError(syscall.Close(syscallFd(f))) +} + +// handleSyscallError converts syscall errors into regular os package errors. +// The err parameter must be either nil or of type syscall.Errno. +func handleSyscallError(err error) error { + if err == nil { + return nil + } + switch err.(syscall.Errno) { + case syscall.EEXIST: + return ErrExist + case syscall.ENOENT: + return ErrNotExist + default: + return err + } +} diff --git a/src/os/file_unix.go b/src/os/file_unix.go index bb77c85a..b83574d5 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -2,113 +2,4 @@ package os -import ( - "io" - "syscall" -) - -func init() { - // Mount the host filesystem at the root directory. This is what most - // programs will be expecting. - Mount("/", unixFilesystem{}) -} - -// Stdin, Stdout, and Stderr are open Files pointing to the standard input, -// standard output, and standard error file descriptors. -var ( - Stdin = &File{unixFileHandle(0), "/dev/stdin"} - Stdout = &File{unixFileHandle(1), "/dev/stdout"} - Stderr = &File{unixFileHandle(2), "/dev/stderr"} -) - -// isOS indicates whether we're running on a real operating system with -// filesystem support. -const isOS = true - -// unixFilesystem is an empty handle for a Unix/Linux filesystem. All operations -// are relative to the current working directory. -type unixFilesystem struct { -} - -func (fs unixFilesystem) Mkdir(path string, perm FileMode) error { - return handleSyscallError(syscall.Mkdir(path, uint32(perm))) -} - -func (fs unixFilesystem) Remove(path string) error { - return handleSyscallError(syscall.Unlink(path)) -} - -func (fs unixFilesystem) OpenFile(path string, flag int, perm FileMode) (FileHandle, error) { - // Map os package flags to syscall flags. - syscallFlag := 0 - if flag&O_RDONLY != 0 { - syscallFlag |= syscall.O_RDONLY - } - if flag&O_WRONLY != 0 { - syscallFlag |= syscall.O_WRONLY - } - if flag&O_RDWR != 0 { - syscallFlag |= syscall.O_RDWR - } - if flag&O_APPEND != 0 { - syscallFlag |= syscall.O_APPEND - } - if flag&O_CREATE != 0 { - syscallFlag |= syscall.O_CREAT - } - if flag&O_EXCL != 0 { - syscallFlag |= syscall.O_EXCL - } - if flag&O_SYNC != 0 { - syscallFlag |= syscall.O_SYNC - } - if flag&O_TRUNC != 0 { - syscallFlag |= syscall.O_TRUNC - } - fp, err := syscall.Open(path, syscallFlag, uint32(perm)) - return unixFileHandle(fp), handleSyscallError(err) -} - -// unixFileHandle is a Unix file pointer with associated methods that implement -// the FileHandle interface. -type unixFileHandle uintptr - -// Read reads up to len(b) bytes from the File. It returns the number of bytes -// read and any error encountered. At end of file, Read returns 0, io.EOF. -func (f unixFileHandle) Read(b []byte) (n int, err error) { - n, err = syscall.Read(int(f), b) - err = handleSyscallError(err) - if n == 0 && err == nil { - err = io.EOF - } - return -} - -// Write writes len(b) bytes to the File. It returns the number of bytes written -// and an error, if any. Write returns a non-nil error when n != len(b). -func (f unixFileHandle) Write(b []byte) (n int, err error) { - n, err = syscall.Write(int(f), b) - err = handleSyscallError(err) - return -} - -// Close closes the File, rendering it unusable for I/O. -func (f unixFileHandle) Close() error { - return handleSyscallError(syscall.Close(int(f))) -} - -// handleSyscallError converts syscall errors into regular os package errors. -// The err parameter must be either nil or of type syscall.Errno. -func handleSyscallError(err error) error { - if err == nil { - return nil - } - switch err.(syscall.Errno) { - case syscall.EEXIST: - return ErrExist - case syscall.ENOENT: - return ErrNotExist - default: - return err - } -} +type syscallFd = int diff --git a/src/os/file_windows.go b/src/os/file_windows.go new file mode 100644 index 00000000..11ae1e0d --- /dev/null +++ b/src/os/file_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package os + +import "syscall" + +type syscallFd = syscall.Handle diff --git a/src/runtime/gc_amd64_windows.S b/src/runtime/gc_amd64_windows.S new file mode 100644 index 00000000..e65352cb --- /dev/null +++ b/src/runtime/gc_amd64_windows.S @@ -0,0 +1,22 @@ +.section .text.tinygo_scanCurrentStack,"ax" +.global tinygo_scanCurrentStack +tinygo_scanCurrentStack: + // Save callee-saved registers. + pushq %rbx + pushq %rbp + pushq %rdi + pushq %rsi + pushq %r12 + pushq %r13 + pushq %r14 + pushq %r15 + + // Scan the stack. + subq $8, %rsp // adjust the stack before the call to maintain 16-byte alignment + movq %rsp, %rdi + callq tinygo_scanstack + + // Restore the stack pointer. Registers do not need to be restored as they + // were only pushed to be discoverable by the GC. + addq $72, %rsp + retq diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go new file mode 100644 index 00000000..85282cc3 --- /dev/null +++ b/src/runtime/os_windows.go @@ -0,0 +1,3 @@ +package runtime + +const GOOS = "windows" diff --git a/src/runtime/runtime_windows.go b/src/runtime/runtime_windows.go new file mode 100644 index 00000000..6872992d --- /dev/null +++ b/src/runtime/runtime_windows.go @@ -0,0 +1,230 @@ +package runtime + +import "unsafe" + +//export abort +func abort() + +//export exit +func libc_exit(code int) + +//export putchar +func libc_putchar(c int) int + +//export VirtualAlloc +func _VirtualAlloc(lpAddress unsafe.Pointer, dwSize uintptr, flAllocationType, flProtect uint32) unsafe.Pointer + +//export QueryUnbiasedInterruptTime +func _QueryUnbiasedInterruptTime(UnbiasedTime *uint64) bool + +// The parameter is really a LPFILETIME, but *uint64 should be compatible. +//export GetSystemTimeAsFileTime +func _GetSystemTimeAsFileTime(lpSystemTimeAsFileTime *uint64) + +//export LoadLibraryExW +func _LoadLibraryExW(lpLibFileName *uint16, hFile uintptr, dwFlags uint32) uintptr + +//export Sleep +func _Sleep(milliseconds uint32) + +const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800 + +//export GetProcAddress +func getProcAddress(handle uintptr, procname *byte) uintptr + +//export _configure_narrow_argv +func _configure_narrow_argv(int32) int32 + +//export __p___argc +func __p___argc() *int32 + +//export __p___argv +func __p___argv() **unsafe.Pointer + +func postinit() {} + +//export mainCRTStartup +func mainCRTStartup() int { + preinit() + + // Obtain the initial stack pointer right before calling the run() function. + // The run function has been moved to a separate (non-inlined) function so + // that the correct stack pointer is read. + stackTop = getCurrentStackPointer() + runMain() + + // For libc compatibility. + return 0 +} + +// Must be a separate function to get the correct stack pointer. +//go:noinline +func runMain() { + run() +} + +var args []string + +//go:linkname os_runtime_args os.runtime_args +func os_runtime_args() []string { + if args == nil { + // Obtain argc/argv from the environment. + _configure_narrow_argv(2) + argc := *__p___argc() + argv := *__p___argv() + + // Make args slice big enough so that it can store all command line + // arguments. + args = make([]string, argc) + + // Initialize command line parameters. + for i := 0; i < int(argc); i++ { + // Convert the C string to a Go string. + length := strlen(*argv) + arg := (*_string)(unsafe.Pointer(&args[i])) + arg.length = length + arg.ptr = (*byte)(*argv) + // This is the Go equivalent of "argv++" in C. + argv = (*unsafe.Pointer)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + unsafe.Sizeof(argv))) + } + } + return args +} + +func putchar(c byte) { + libc_putchar(int(c)) +} + +var heapSize uintptr = 128 * 1024 // small amount to start +var heapMaxSize uintptr + +var heapStart, heapEnd uintptr + +func preinit() { + // Allocate a large chunk of virtual memory. Because it is virtual, it won't + // really be allocated in RAM. Memory will only be allocated when it is + // first touched. + heapMaxSize = 1 * 1024 * 1024 * 1024 // 1GB for the entire heap + const ( + MEM_COMMIT = 0x00001000 + MEM_RESERVE = 0x00002000 + PAGE_READWRITE = 0x04 + ) + heapStart = uintptr(_VirtualAlloc(nil, heapMaxSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)) + heapEnd = heapStart + heapSize +} + +type timeUnit int64 + +var stackTop uintptr + +func ticksToNanoseconds(ticks timeUnit) int64 { + // Interrupt time count works in units of 100 nanoseconds. + return int64(ticks) * 100 +} + +func nanosecondsToTicks(ns int64) timeUnit { + // Interrupt time count works in units of 100 nanoseconds. + return timeUnit(ns) / 100 +} + +func sleepTicks(d timeUnit) { + // Calcluate milliseconds from ticks (which have a resolution of 100ns), + // rounding up. + milliseconds := int64(d+9_999) / 10_000 + for milliseconds != 0 { + duration := uint32(milliseconds) + _Sleep(duration) + milliseconds -= int64(duration) + } +} + +func ticks() timeUnit { + var unbiasedTime uint64 + _QueryUnbiasedInterruptTime(&unbiasedTime) + return timeUnit(unbiasedTime) +} + +//go:linkname now time.now +func now() (sec int64, nsec int32, mono int64) { + // Get the current time in Windows "file time" format. + var time uint64 + _GetSystemTimeAsFileTime(&time) + + // Convert file time to Unix time. + // According to the documentation: + // > Contains a 64-bit value representing the number of 100-nanosecond + // > intervals since January 1, 1601 (UTC). + // We'll convert it to 100 nanosecond intervals starting at 1970. + const ( + // number of 100-nanosecond intervals in a second + intervalsPerSecond = 10_000_000 + secondsPerDay = 60 * 60 * 24 + // Number of days between the Windows epoch (1 january 1601) and the + // Unix epoch (1 january 1970). Source: + // https://www.wolframalpha.com/input/?i=days+between+1+january+1601+and+1+january+1970 + days = 134774 + ) + time -= days * secondsPerDay * intervalsPerSecond + + // Convert the time (in 100ns units) to sec/nsec/mono as expected by the + // time package. + sec = int64(time / intervalsPerSecond) + nsec = int32((time - (uint64(sec) * intervalsPerSecond)) * 100) + mono = ticksToNanoseconds(ticks()) + return +} + +//go:linkname syscall_Exit syscall.Exit +func syscall_Exit(code int) { + libc_exit(code) +} + +func growHeap() bool { + if heapSize == heapMaxSize { + // Already at the max. If we run out of memory, we should consider + // increasing heapMaxSize.. + return false + } + // Grow the heap size used by the program. + heapSize = (heapSize * 4 / 3) &^ 4095 // grow by around 33% + if heapSize > heapMaxSize { + heapSize = heapMaxSize + } + setHeapEnd(heapStart + heapSize) + return true +} + +//go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary +func syscall_loadsystemlibrary(filename *uint16, absoluteFilepath *uint16) (handle, err uintptr) { + handle = _LoadLibraryExW(filename, 0, _LOAD_LIBRARY_SEARCH_SYSTEM32) + if handle == 0 { + panic("todo: get error") + } + return +} + +//go:linkname syscall_loadlibrary syscall.loadlibrary +func syscall_loadlibrary(filename *uint16) (handle, err uintptr) { + panic("todo: syscall.loadlibrary") +} + +//go:linkname syscall_getprocaddress syscall.getprocaddress +func syscall_getprocaddress(handle uintptr, procname *byte) (outhandle, err uintptr) { + outhandle = getProcAddress(handle, procname) + if outhandle == 0 { + panic("todo: get error") + } + return +} + +// TinyGo does not yet support any form of parallelism on Windows, so these can +// be left empty. + +//go:linkname procPin sync/atomic.runtime_procPin +func procPin() { +} + +//go:linkname procUnpin sync/atomic.runtime_procUnpin +func procUnpin() { +} diff --git a/src/syscall/syscall_libc_darwin.go b/src/syscall/syscall_libc_darwin.go index 3a379413..fb8655c8 100644 --- a/src/syscall/syscall_libc_darwin.go +++ b/src/syscall/syscall_libc_darwin.go @@ -62,6 +62,12 @@ const ( SIGTERM Signal = 0xf ) +const ( + Stdin = 0 + Stdout = 1 + Stderr = 2 +) + const ( O_RDONLY = 0x0 O_WRONLY = 0x1 diff --git a/testdata/filesystem.go b/testdata/filesystem.go index dc5b9a53..5f41318e 100644 --- a/testdata/filesystem.go +++ b/testdata/filesystem.go @@ -33,6 +33,5 @@ func main() { panic(err) } - print(string(data)) - + os.Stdout.Write(data) }