Skip to content

Commit df0f5ae

Browse files
aykevldeadprogram
authored andcommitted
windows: add ARM64 support
This was actually surprising once I got TinyGo to build on Windows 11 ARM64. All the changes are exactly what you'd expect for a new architecture, there was no special weirdness just for arm64. Actually getting TinyGo to build was kind of involved though. The very short summary is: install arm64 versions of some pieces of software (like golang, cmake) instead of installing them though choco. In particular, use the llvm-mingw[1] toolchain instead of using standard mingw. [1]: https://github.com/mstorsjo/llvm-mingw/releases
1 parent 45c8817 commit df0f5ae

File tree

11 files changed

+206
-10
lines changed

11 files changed

+206
-10
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ endif
738738
@$(MD5SUM) test.hex
739739
GOOS=linux GOARCH=arm $(TINYGO) build -size short -o test.elf ./testdata/cgo
740740
GOOS=windows GOARCH=amd64 $(TINYGO) build -size short -o test.exe ./testdata/cgo
741+
GOOS=windows GOARCH=arm64 $(TINYGO) build -size short -o test.exe ./testdata/cgo
741742
GOOS=darwin GOARCH=amd64 $(TINYGO) build -size short -o test ./testdata/cgo
742743
GOOS=darwin GOARCH=arm64 $(TINYGO) build -size short -o test ./testdata/cgo
743744
ifneq ($(OS),Windows_NT)

builder/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
151151
return BuildResult{}, err
152152
}
153153
unlock()
154-
libcDependencies = append(libcDependencies, makeMinGWExtraLibs(tmpdir)...)
154+
libcDependencies = append(libcDependencies, makeMinGWExtraLibs(tmpdir, config.GOARCH())...)
155155
case "":
156156
// no library specified, so nothing to do
157157
default:

builder/builder_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func TestClangAttributes(t *testing.T) {
5959
{GOOS: "darwin", GOARCH: "amd64"},
6060
{GOOS: "darwin", GOARCH: "arm64"},
6161
{GOOS: "windows", GOARCH: "amd64"},
62+
{GOOS: "windows", GOARCH: "arm64"},
6263
} {
6364
name := "GOOS=" + options.GOOS + ",GOARCH=" + options.GOARCH
6465
if options.GOARCH == "arm" {

builder/mingw-w64.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package builder
22

33
import (
4+
"fmt"
45
"io"
56
"os"
67
"path/filepath"
@@ -43,7 +44,7 @@ var MinGW = Library{
4344
//
4445
// TODO: cache the result. At the moment, it costs a few hundred milliseconds to
4546
// compile these files.
46-
func makeMinGWExtraLibs(tmpdir string) []*compileJob {
47+
func makeMinGWExtraLibs(tmpdir, goarch string) []*compileJob {
4748
var jobs []*compileJob
4849
root := goenv.Get("TINYGOROOT")
4950
// Normally all the api-ms-win-crt-*.def files are all compiled to a single
@@ -74,16 +75,27 @@ func makeMinGWExtraLibs(tmpdir string) []*compileJob {
7475
result: outpath,
7576
run: func(job *compileJob) error {
7677
defpath := inpath
78+
var archDef, emulation string
79+
switch goarch {
80+
case "amd64":
81+
archDef = "-DDEF_X64"
82+
emulation = "i386pep"
83+
case "arm64":
84+
archDef = "-DDEF_ARM64"
85+
emulation = "arm64pe"
86+
default:
87+
return fmt.Errorf("unsupported architecture for mingw-w64: %s", goarch)
88+
}
7789
if strings.HasSuffix(inpath, ".in") {
7890
// .in files need to be preprocessed by a preprocessor (-E)
7991
// first.
8092
defpath = outpath + ".def"
81-
err := runCCompiler("-E", "-x", "c", "-Wp,-w", "-P", "-DDEF_X64", "-DDATA", "-o", defpath, inpath, "-I"+goenv.Get("TINYGOROOT")+"/lib/mingw-w64/mingw-w64-crt/def-include/")
93+
err := runCCompiler("-E", "-x", "c", "-Wp,-w", "-P", archDef, "-DDATA", "-o", defpath, inpath, "-I"+goenv.Get("TINYGOROOT")+"/lib/mingw-w64/mingw-w64-crt/def-include/")
8294
if err != nil {
8395
return err
8496
}
8597
}
86-
return link("ld.lld", "-m", "i386pep", "-o", outpath, defpath)
98+
return link("ld.lld", "-m", emulation, "-o", outpath, defpath)
8799
},
88100
}
89101
jobs = append(jobs, job)

builder/tools-builtin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const hasBuiltinTools = true
2828
func RunTool(tool string, args ...string) error {
2929
linker := "elf"
3030
if tool == "ld.lld" && len(args) >= 2 {
31-
if args[0] == "-m" && args[1] == "i386pep" {
31+
if args[0] == "-m" && (args[1] == "i386pep" || args[1] == "arm64pe") {
3232
linker = "mingw"
3333
} else if args[0] == "-flavor" {
3434
linker = args[1]

compileopts/target.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,19 @@ func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) {
304304
// normally present in Go (without explicitly opting in).
305305
// For more discussion:
306306
// https://groups.google.com/g/Golang-nuts/c/Jd9tlNc6jUE/m/Zo-7zIP_m3MJ?pli=1
307+
switch goarch {
308+
case "amd64":
309+
spec.LDFlags = append(spec.LDFlags,
310+
"-m", "i386pep",
311+
"--image-base", "0x400000",
312+
)
313+
case "arm64":
314+
spec.LDFlags = append(spec.LDFlags,
315+
"-m", "arm64pe",
316+
)
317+
}
307318
spec.LDFlags = append(spec.LDFlags,
308-
"-m", "i386pep",
309319
"-Bdynamic",
310-
"--image-base", "0x400000",
311320
"--gc-sections",
312321
"--no-insert-timestamp",
313322
"--no-dynamicbase",

compiler/defer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ mov x0, #0
162162
1:
163163
`
164164
constraints = "={x0},{x1},~{x1},~{x2},~{x3},~{x4},~{x5},~{x6},~{x7},~{x8},~{x9},~{x10},~{x11},~{x12},~{x13},~{x14},~{x15},~{x16},~{x17},~{x19},~{x20},~{x21},~{x22},~{x23},~{x24},~{x25},~{x26},~{x27},~{x28},~{lr},~{q0},~{q1},~{q2},~{q3},~{q4},~{q5},~{q6},~{q7},~{q8},~{q9},~{q10},~{q11},~{q12},~{q13},~{q14},~{q15},~{q16},~{q17},~{q18},~{q19},~{q20},~{q21},~{q22},~{q23},~{q24},~{q25},~{q26},~{q27},~{q28},~{q29},~{q30},~{nzcv},~{ffr},~{vg},~{memory}"
165-
if b.GOOS != "darwin" {
165+
if b.GOOS != "darwin" && b.GOOS != "windows" {
166166
// These registers cause the following warning when compiling for
167-
// MacOS:
167+
// MacOS and Windows:
168168
// warning: inline asm clobber list contains reserved registers:
169169
// X18, FP
170170
// Reserved registers on the clobber list may not be preserved

src/internal/task/task_stack_arm64.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.tasks && arm64
1+
//go:build scheduler.tasks && arm64 && !windows
22

33
package task
44

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.section .text.tinygo_startTask,"ax"
2+
.global tinygo_startTask
3+
tinygo_startTask:
4+
.cfi_startproc
5+
// Small assembly stub for starting a goroutine. This is already run on the
6+
// new stack, with the callee-saved registers already loaded.
7+
// Most importantly, x19 contains the pc of the to-be-started function and
8+
// x20 contains the only argument it is given. Multiple arguments are packed
9+
// into one by storing them in a new allocation.
10+
11+
// Indicate to the unwinder that there is nothing to unwind, this is the
12+
// root frame. It avoids the following (bogus) error message in GDB:
13+
// Backtrace stopped: previous frame identical to this frame (corrupt stack?)
14+
.cfi_undefined lr
15+
16+
// Set the first argument of the goroutine start wrapper, which contains all
17+
// the arguments.
18+
mov x0, x20
19+
20+
// Branch to the "goroutine start" function. By using blx instead of bx,
21+
// we'll return here instead of tail calling.
22+
blr x19
23+
24+
// After return, exit this goroutine. This is a tail call.
25+
b tinygo_pause
26+
.cfi_endproc
27+
28+
29+
.global tinygo_swapTask
30+
tinygo_swapTask:
31+
// This function gets the following parameters:
32+
// x0 = newStack uintptr
33+
// x1 = oldStack *uintptr
34+
35+
// Save all callee-saved registers:
36+
stp x19, x20, [sp, #-160]!
37+
stp x21, x22, [sp, #16]
38+
stp x23, x24, [sp, #32]
39+
stp x25, x26, [sp, #48]
40+
stp x27, x28, [sp, #64]
41+
stp x29, x30, [sp, #80]
42+
stp d8, d9, [sp, #96]
43+
stp d10, d11, [sp, #112]
44+
stp d12, d13, [sp, #128]
45+
stp d14, d15, [sp, #144]
46+
47+
// Save the current stack pointer in oldStack.
48+
mov x8, sp
49+
str x8, [x1]
50+
51+
// Switch to the new stack pointer.
52+
mov sp, x0
53+
54+
// Restore stack state and return.
55+
ldp d14, d15, [sp, #144]
56+
ldp d12, d13, [sp, #128]
57+
ldp d10, d11, [sp, #112]
58+
ldp d8, d9, [sp, #96]
59+
ldp x29, x30, [sp, #80]
60+
ldp x27, x28, [sp, #64]
61+
ldp x25, x26, [sp, #48]
62+
ldp x23, x24, [sp, #32]
63+
ldp x21, x22, [sp, #16]
64+
ldp x19, x20, [sp], #160
65+
ret
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//go:build scheduler.tasks && arm64 && windows
2+
3+
package task
4+
5+
import "unsafe"
6+
7+
var systemStack uintptr
8+
9+
// calleeSavedRegs is the list of registers that must be saved and restored
10+
// when switching between tasks. Also see task_stack_arm64_windows.S that
11+
// relies on the exact layout of this struct.
12+
type calleeSavedRegs struct {
13+
x19 uintptr
14+
x20 uintptr
15+
x21 uintptr
16+
x22 uintptr
17+
x23 uintptr
18+
x24 uintptr
19+
x25 uintptr
20+
x26 uintptr
21+
x27 uintptr
22+
x28 uintptr
23+
x29 uintptr
24+
pc uintptr // aka x30 aka LR
25+
26+
d8 uintptr
27+
d9 uintptr
28+
d10 uintptr
29+
d11 uintptr
30+
d12 uintptr
31+
d13 uintptr
32+
d14 uintptr
33+
d15 uintptr
34+
}
35+
36+
// archInit runs architecture-specific setup for the goroutine startup.
37+
func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) {
38+
// Store the initial sp for the startTask function (implemented in assembly).
39+
s.sp = uintptr(unsafe.Pointer(r))
40+
41+
// Initialize the registers.
42+
// These will be popped off of the stack on the first resume of the goroutine.
43+
44+
// Start the function at tinygo_startTask (defined in src/internal/task/task_stack_arm64_windows.S).
45+
// This assembly code calls a function (passed in x19) with a single argument
46+
// (passed in x20). After the function returns, it calls Pause().
47+
r.pc = uintptr(unsafe.Pointer(&startTask))
48+
49+
// Pass the function to call in x19.
50+
// This function is a compiler-generated wrapper which loads arguments out of a struct pointer.
51+
// See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information.
52+
r.x19 = fn
53+
54+
// Pass the pointer to the arguments struct in x20.
55+
r.x20 = uintptr(args)
56+
}
57+
58+
func (s *state) resume() {
59+
swapTask(s.sp, &systemStack)
60+
}
61+
62+
func (s *state) pause() {
63+
newStack := systemStack
64+
systemStack = 0
65+
swapTask(newStack, &s.sp)
66+
}
67+
68+
// SystemStack returns the system stack pointer when called from a task stack.
69+
// When called from the system stack, it returns 0.
70+
func SystemStack() uintptr {
71+
return systemStack
72+
}

0 commit comments

Comments
 (0)