detached 6f34d83

purego: add support for riscv64 (#385)

This change adds the neccessary code logic to support RISCV64 with both CGO enabled/disabled.

Closes #382
commit
6f34d839a741ceeab45dd7fb86b9f8fc17eb318d
author
George Adams · · 2026-01-21 18:42 UTC
committer
GitHub · · 2026-01-21 18:42 UTC
parent
ef2baba
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
16 files · +6552 -20
M .github/workflows/test.yml +16 -0
@@ -120,6 +120,22 @@ jobs:
120120 sudo apt-get install g++-multilib
121121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
122122
123+ - name: go test (Linux riscv64)
124+ if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') && !startsWith(matrix.go, '1.20.') && !startsWith(matrix.go, '1.21.') }}
125+ run: |
126+ sudo apt-get update
127+ sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu qemu-user
128+ go env -w CC=riscv64-linux-gnu-gcc
129+ go env -w CXX=riscv64-linux-gnu-g++
130+ # Create symlink for the dynamic linker that QEMU expects
131+ sudo ln -sf /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 /lib/ld.so.1
132+ env GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
133+ env QEMU_LD_PREFIX=/usr/riscv64-linux-gnu qemu-riscv64 ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
134+ env GOOS=linux GOARCH=riscv64 CGO_ENABLED=1 go test -c -o=purego-test-cgo .
135+ env QEMU_LD_PREFIX=/usr/riscv64-linux-gnu qemu-riscv64 ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
136+ go env -u CC
137+ go env -u CXX
138+
123139 - name: go test race (no Cgo)
124140 if: ${{ runner.os == 'macOS' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }}
125141 run: |
@@ -120,6 +120,22 @@ jobs:
120 sudo apt-get install g++-multilib120 sudo apt-get install g++-multilib
121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
122 122
123+ - name: go test (Linux riscv64)
124+ if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') && !startsWith(matrix.go, '1.20.') && !startsWith(matrix.go, '1.21.') }}
125+ run: |
126+ sudo apt-get update
127+ sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu qemu-user
128+ go env -w CC=riscv64-linux-gnu-gcc
129+ go env -w CXX=riscv64-linux-gnu-g++
130+ # Create symlink for the dynamic linker that QEMU expects
131+ sudo ln -sf /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 /lib/ld.so.1
132+ env GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
133+ env QEMU_LD_PREFIX=/usr/riscv64-linux-gnu qemu-riscv64 ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
134+ env GOOS=linux GOARCH=riscv64 CGO_ENABLED=1 go test -c -o=purego-test-cgo .
135+ env QEMU_LD_PREFIX=/usr/riscv64-linux-gnu qemu-riscv64 ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
136+ go env -u CC
137+ go env -u CXX
138+
123 - name: go test race (no Cgo)139 - name: go test race (no Cgo)
124 if: ${{ runner.os == 'macOS' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }}140 if: ${{ runner.os == 'macOS' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }}
125 run: |141 run: |
M README.md +1 -1
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
4242
4343 - **Android**: 386, arm
4444 - **FreeBSD**: amd64, arm64
45-- **Linux**: 386, arm, loong64
45+- **Linux**: 386, arm, loong64, riscv64
4646 - **Windows**: 386*, arm*
4747
4848 `*` These architectures only support `SyscallN` and `NewCallback`
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
42 42
43 - **Android**: 386, arm43 - **Android**: 386, arm
44 - **FreeBSD**: amd64, arm6444 - **FreeBSD**: amd64, arm64
45-- **Linux**: 386, arm, loong6445+- **Linux**: 386, arm, loong64, riscv64
46 - **Windows**: 386*, arm*46 - **Windows**: 386*, arm*
47 47
48 `*` These architectures only support `SyscallN` and `NewCallback`48 `*` These architectures only support `SyscallN` and `NewCallback`
M callback_test.go +1 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
33
4-//go:build darwin || (linux && (amd64 || arm64 || loong64))
4+//go:build darwin || (linux && (amd64 || arm64 || loong64 || riscv64))
55
66 package purego_test
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3 3
4-//go:build darwin || (linux && (amd64 || arm64 || loong64))4+//go:build darwin || (linux && (amd64 || arm64 || loong64 || riscv64))
5 5
6 package purego_test6 package purego_test
7 7
M func.go +7 -7
@@ -132,7 +132,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
132132 panic("purego: cfn is nil")
133133 }
134134 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
135- runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {
135+ runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
136136 panic("purego: float returns are not supported")
137137 }
138138 {
@@ -281,7 +281,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
281281 var arm64_r8 uintptr
282282 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
283283 outType := ty.Out(0)
284- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64") && outType.Size() > maxRegAllocStructSize {
284+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64") && outType.Size() > maxRegAllocStructSize {
285285 val := reflect.New(outType)
286286 keepAlive = append(keepAlive, val)
287287 addInt(val.Pointer())
@@ -323,7 +323,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
323323 syscall := thePool.Get().(*syscall15Args)
324324 defer thePool.Put(syscall)
325325
326- if runtime.GOARCH == "loong64" {
326+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" {
327327 *syscall = syscall15Args{
328328 cfn,
329329 sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
@@ -490,7 +490,7 @@ func roundUpTo8(val uintptr) uintptr {
490490
491491 func numOfIntegerRegisters() int {
492492 switch runtime.GOARCH {
493- case "arm64", "loong64":
493+ case "arm64", "loong64", "riscv64":
494494 return 8
495495 case "amd64":
496496 return 6
@@ -537,9 +537,9 @@ func estimateStackBytes(ty reflect.Type) int {
537537 // tight packing as normal C function calls.
538538 func isCallbackFunction(cfn uintptr) bool {
539539 // Only platforms with syscall_sysv.go have callback detection.
540- // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd
540+ // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd
541541 hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||
542- (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64"))
542+ (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64"))
543543 if !hasSyscallSysv {
544544 return false
545545 }
@@ -549,7 +549,7 @@ func isCallbackFunction(cfn uintptr) bool {
549549 switch runtime.GOARCH {
550550 case "386", "amd64":
551551 entrySize = 5
552- case "arm", "arm64", "loong64":
552+ case "arm", "arm64", "loong64", "riscv64":
553553 entrySize = 8
554554 default:
555555 return false
@@ -132,7 +132,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
132 panic("purego: cfn is nil")132 panic("purego: cfn is nil")
133 }133 }
134 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&134 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
135- runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {135+ runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
136 panic("purego: float returns are not supported")136 panic("purego: float returns are not supported")
137 }137 }
138 {138 {
@@ -281,7 +281,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
281 var arm64_r8 uintptr281 var arm64_r8 uintptr
282 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {282 if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
283 outType := ty.Out(0)283 outType := ty.Out(0)
284- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64") && outType.Size() > maxRegAllocStructSize {284+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64") && outType.Size() > maxRegAllocStructSize {
285 val := reflect.New(outType)285 val := reflect.New(outType)
286 keepAlive = append(keepAlive, val)286 keepAlive = append(keepAlive, val)
287 addInt(val.Pointer())287 addInt(val.Pointer())
@@ -323,7 +323,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
323 syscall := thePool.Get().(*syscall15Args)323 syscall := thePool.Get().(*syscall15Args)
324 defer thePool.Put(syscall)324 defer thePool.Put(syscall)
325 325
326- if runtime.GOARCH == "loong64" {326+ if runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" {
327 *syscall = syscall15Args{327 *syscall = syscall15Args{
328 cfn,328 cfn,
329 sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],329 sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
@@ -490,7 +490,7 @@ func roundUpTo8(val uintptr) uintptr {
490 490
491 func numOfIntegerRegisters() int {491 func numOfIntegerRegisters() int {
492 switch runtime.GOARCH {492 switch runtime.GOARCH {
493- case "arm64", "loong64":493+ case "arm64", "loong64", "riscv64":
494 return 8494 return 8
495 case "amd64":495 case "amd64":
496 return 6496 return 6
@@ -537,9 +537,9 @@ func estimateStackBytes(ty reflect.Type) int {
537 // tight packing as normal C function calls.537 // tight packing as normal C function calls.
538 func isCallbackFunction(cfn uintptr) bool {538 func isCallbackFunction(cfn uintptr) bool {
539 // Only platforms with syscall_sysv.go have callback detection.539 // Only platforms with syscall_sysv.go have callback detection.
540- // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd540+ // Match the build constraint: darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd
541 hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||541 hasSyscallSysv := runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" ||
542- (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64"))542+ (runtime.GOOS == "linux" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64"))
543 if !hasSyscallSysv {543 if !hasSyscallSysv {
544 return false544 return false
545 }545 }
@@ -549,7 +549,7 @@ func isCallbackFunction(cfn uintptr) bool {
549 switch runtime.GOARCH {549 switch runtime.GOARCH {
550 case "386", "amd64":550 case "386", "amd64":
551 entrySize = 5551 entrySize = 5
552- case "arm", "arm64", "loong64":552+ case "arm", "arm64", "loong64", "riscv64":
553 entrySize = 8553 entrySize = 8
554 default:554 default:
555 return false555 return false
M func_test.go +4 -4
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
5050 }
5151
5252 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {
53+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
5454 t.Skip("Platform doesn't support Floats")
5555 return
5656 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
7979 }
8080
8181 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {
82+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
8383 t.Skip("Platform doesn't support Floats")
8484 return
8585 }
@@ -118,7 +118,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
118118 }
119119
120120 func TestRegisterLibFunc_Bool(t *testing.T) {
121- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {
121+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
122122 t.Skip("Platform doesn't support callbacks")
123123 return
124124 }
@@ -373,7 +373,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
373373 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
374374 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
375375 }
376- if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") {
376+ if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64") {
377377 t.Skip("float32 stack arguments not yet supported on this platform")
378378 }
379379 // Struct tests require Darwin ARM64 or AMD64
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
50 }50 }
51 51
52 func Test_qsort(t *testing.T) {52 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {53+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
54 t.Skip("Platform doesn't support Floats")54 t.Skip("Platform doesn't support Floats")
55 return55 return
56 }56 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
79 }79 }
80 80
81 func TestRegisterFunc_Floats(t *testing.T) {81 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {82+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
83 t.Skip("Platform doesn't support Floats")83 t.Skip("Platform doesn't support Floats")
84 return84 return
85 }85 }
@@ -118,7 +118,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
118 }118 }
119 119
120 func TestRegisterLibFunc_Bool(t *testing.T) {120 func TestRegisterLibFunc_Bool(t *testing.T) {
121- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" {121+ if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
122 t.Skip("Platform doesn't support callbacks")122 t.Skip("Platform doesn't support callbacks")
123 return123 return
124 }124 }
@@ -373,7 +373,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
373 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {373 if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
374 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")374 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
375 }375 }
376- if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64") {376+ if tt.name == "10_float32" && (runtime.GOARCH == "386" || runtime.GOARCH == "arm" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64") {
377 t.Skip("float32 stack arguments not yet supported on this platform")377 t.Skip("float32 stack arguments not yet supported on this platform")
378 }378 }
379 // Struct tests require Darwin ARM64 or AMD64379 // Struct tests require Darwin ARM64 or AMD64
M internal/cgo/syscall_cgo_unix.go +1 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build freebsd || (linux && !(arm64 || amd64 || loong64)) || netbsd
4+//go:build freebsd || (linux && !(arm64 || amd64 || loong64 || riscv64)) || netbsd
55
66 package cgo
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build freebsd || (linux && !(arm64 || amd64 || loong64)) || netbsd4+//go:build freebsd || (linux && !(arm64 || amd64 || loong64 || riscv64)) || netbsd
5 5
6 package cgo6 package cgo
7 7
A internal/fakecgo/asm_riscv64.s +78 -0
@@ -0,0 +1,78 @@
1+// Copyright 2020 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB),NOSPLIT|NOFRAME,$0
12+ /*
13+ * Push arguments for fn (X10, X11, X13), along with all callee-save
14+ * registers. Note that at procedure entry the first argument is at
15+ * 8(X2).
16+ */
17+ ADD $(-8*29), X2
18+ MOV X10, (8*1)(X2) // fn unsafe.Pointer
19+ MOV X11, (8*2)(X2) // a unsafe.Pointer
20+ MOV X13, (8*3)(X2) // ctxt uintptr
21+ MOV X8, (8*4)(X2)
22+ MOV X9, (8*5)(X2)
23+ MOV X18, (8*6)(X2)
24+ MOV X19, (8*7)(X2)
25+ MOV X20, (8*8)(X2)
26+ MOV X21, (8*9)(X2)
27+ MOV X22, (8*10)(X2)
28+ MOV X23, (8*11)(X2)
29+ MOV X24, (8*12)(X2)
30+ MOV X25, (8*13)(X2)
31+ MOV X26, (8*14)(X2)
32+ MOV g, (8*15)(X2)
33+ MOV X1, (8*16)(X2)
34+ MOVD F8, (8*17)(X2)
35+ MOVD F9, (8*18)(X2)
36+ MOVD F18, (8*19)(X2)
37+ MOVD F19, (8*20)(X2)
38+ MOVD F20, (8*21)(X2)
39+ MOVD F21, (8*22)(X2)
40+ MOVD F22, (8*23)(X2)
41+ MOVD F23, (8*24)(X2)
42+ MOVD F24, (8*25)(X2)
43+ MOVD F25, (8*26)(X2)
44+ MOVD F26, (8*27)(X2)
45+ MOVD F27, (8*28)(X2)
46+
47+ // Initialize Go ABI environment
48+ CALL runtime·load_g(SB)
49+ CALL runtime·cgocallback(SB)
50+
51+ MOV (8*4)(X2), X8
52+ MOV (8*5)(X2), X9
53+ MOV (8*6)(X2), X18
54+ MOV (8*7)(X2), X19
55+ MOV (8*8)(X2), X20
56+ MOV (8*9)(X2), X21
57+ MOV (8*10)(X2), X22
58+ MOV (8*11)(X2), X23
59+ MOV (8*12)(X2), X24
60+ MOV (8*13)(X2), X25
61+ MOV (8*14)(X2), X26
62+ MOV (8*15)(X2), g
63+ MOV (8*16)(X2), X1
64+ MOVD (8*17)(X2), F8
65+ MOVD (8*18)(X2), F9
66+ MOVD (8*19)(X2), F18
67+ MOVD (8*20)(X2), F19
68+ MOVD (8*21)(X2), F20
69+ MOVD (8*22)(X2), F21
70+ MOVD (8*23)(X2), F22
71+ MOVD (8*24)(X2), F23
72+ MOVD (8*25)(X2), F24
73+ MOVD (8*26)(X2), F25
74+ MOVD (8*27)(X2), F26
75+ MOVD (8*28)(X2), F27
76+ ADD $(8*29), X2
77+
78+ RET
@@ -0,0 +1,78 @@
1+// Copyright 2020 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB),NOSPLIT|NOFRAME,$0
12+ /*
13+ * Push arguments for fn (X10, X11, X13), along with all callee-save
14+ * registers. Note that at procedure entry the first argument is at
15+ * 8(X2).
16+ */
17+ ADD $(-8*29), X2
18+ MOV X10, (8*1)(X2) // fn unsafe.Pointer
19+ MOV X11, (8*2)(X2) // a unsafe.Pointer
20+ MOV X13, (8*3)(X2) // ctxt uintptr
21+ MOV X8, (8*4)(X2)
22+ MOV X9, (8*5)(X2)
23+ MOV X18, (8*6)(X2)
24+ MOV X19, (8*7)(X2)
25+ MOV X20, (8*8)(X2)
26+ MOV X21, (8*9)(X2)
27+ MOV X22, (8*10)(X2)
28+ MOV X23, (8*11)(X2)
29+ MOV X24, (8*12)(X2)
30+ MOV X25, (8*13)(X2)
31+ MOV X26, (8*14)(X2)
32+ MOV g, (8*15)(X2)
33+ MOV X1, (8*16)(X2)
34+ MOVD F8, (8*17)(X2)
35+ MOVD F9, (8*18)(X2)
36+ MOVD F18, (8*19)(X2)
37+ MOVD F19, (8*20)(X2)
38+ MOVD F20, (8*21)(X2)
39+ MOVD F21, (8*22)(X2)
40+ MOVD F22, (8*23)(X2)
41+ MOVD F23, (8*24)(X2)
42+ MOVD F24, (8*25)(X2)
43+ MOVD F25, (8*26)(X2)
44+ MOVD F26, (8*27)(X2)
45+ MOVD F27, (8*28)(X2)
46+
47+ // Initialize Go ABI environment
48+ CALL runtime·load_g(SB)
49+ CALL runtime·cgocallback(SB)
50+
51+ MOV (8*4)(X2), X8
52+ MOV (8*5)(X2), X9
53+ MOV (8*6)(X2), X18
54+ MOV (8*7)(X2), X19
55+ MOV (8*8)(X2), X20
56+ MOV (8*9)(X2), X21
57+ MOV (8*10)(X2), X22
58+ MOV (8*11)(X2), X23
59+ MOV (8*12)(X2), X24
60+ MOV (8*13)(X2), X25
61+ MOV (8*14)(X2), X26
62+ MOV (8*15)(X2), g
63+ MOV (8*16)(X2), X1
64+ MOVD (8*17)(X2), F8
65+ MOVD (8*18)(X2), F9
66+ MOVD (8*19)(X2), F18
67+ MOVD (8*20)(X2), F19
68+ MOVD (8*21)(X2), F20
69+ MOVD (8*22)(X2), F21
70+ MOVD (8*23)(X2), F22
71+ MOVD (8*24)(X2), F23
72+ MOVD (8*25)(X2), F24
73+ MOVD (8*26)(X2), F25
74+ MOVD (8*27)(X2), F26
75+ MOVD (8*28)(X2), F27
76+ ADD $(8*29), X2
77+
78+ RET
A internal/fakecgo/trampolines_riscv64.s +71 -0
@@ -0,0 +1,71 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+
9+// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions.
10+
11+TEXT x_cgo_init_trampoline(SB), NOSPLIT, $16
12+ MOV X10, 8(SP)
13+ MOV X11, 16(SP)
14+ MOV ·x_cgo_init_call(SB), X5
15+ MOV (X5), X6
16+ JALR RA, 0(X6)
17+ RET
18+
19+TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8
20+ MOV X10, 8(SP)
21+ MOV ·x_cgo_thread_start_call(SB), X5
22+ MOV (X5), X6
23+ JALR RA, 0(X6)
24+ RET
25+
26+TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8
27+ MOV X10, 8(SP)
28+ MOV ·x_cgo_setenv_call(SB), X5
29+ MOV (X5), X6
30+ JALR RA, 0(X6)
31+ RET
32+
33+TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8
34+ MOV X10, 8(SP)
35+ MOV ·x_cgo_unsetenv_call(SB), X5
36+ MOV (X5), X6
37+ JALR RA, 0(X6)
38+ RET
39+
40+TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0
41+ CALL ·x_cgo_notify_runtime_init_done(SB)
42+ RET
43+
44+TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0
45+ CALL ·x_cgo_bindm(SB)
46+ RET
47+
48+// func setg_trampoline(setg uintptr, g uintptr)
49+TEXT ·setg_trampoline(SB), NOSPLIT, $0
50+ MOV gp+8(FP), X10
51+ MOV setg+0(FP), X5
52+ JALR RA, 0(X5)
53+ RET
54+
55+TEXT threadentry_trampoline(SB), NOSPLIT, $16
56+ MOV X10, 8(SP)
57+ MOV ·threadentry_call(SB), X5
58+ MOV (X5), X6
59+ JALR RA, 0(X6)
60+ RET
61+
62+TEXT ·call5(SB), NOSPLIT, $0-48
63+ MOV fn+0(FP), X9
64+ MOV a1+8(FP), X10
65+ MOV a2+16(FP), X11
66+ MOV a3+24(FP), X12
67+ MOV a4+32(FP), X13
68+ MOV a5+40(FP), X14
69+ JALR RA, 0(X9)
70+ MOV X10, ret+48(FP)
71+ RET
@@ -0,0 +1,71 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+
9+// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions.
10+
11+TEXT x_cgo_init_trampoline(SB), NOSPLIT, $16
12+ MOV X10, 8(SP)
13+ MOV X11, 16(SP)
14+ MOV ·x_cgo_init_call(SB), X5
15+ MOV (X5), X6
16+ JALR RA, 0(X6)
17+ RET
18+
19+TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8
20+ MOV X10, 8(SP)
21+ MOV ·x_cgo_thread_start_call(SB), X5
22+ MOV (X5), X6
23+ JALR RA, 0(X6)
24+ RET
25+
26+TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8
27+ MOV X10, 8(SP)
28+ MOV ·x_cgo_setenv_call(SB), X5
29+ MOV (X5), X6
30+ JALR RA, 0(X6)
31+ RET
32+
33+TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8
34+ MOV X10, 8(SP)
35+ MOV ·x_cgo_unsetenv_call(SB), X5
36+ MOV (X5), X6
37+ JALR RA, 0(X6)
38+ RET
39+
40+TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0
41+ CALL ·x_cgo_notify_runtime_init_done(SB)
42+ RET
43+
44+TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0
45+ CALL ·x_cgo_bindm(SB)
46+ RET
47+
48+// func setg_trampoline(setg uintptr, g uintptr)
49+TEXT ·setg_trampoline(SB), NOSPLIT, $0
50+ MOV gp+8(FP), X10
51+ MOV setg+0(FP), X5
52+ JALR RA, 0(X5)
53+ RET
54+
55+TEXT threadentry_trampoline(SB), NOSPLIT, $16
56+ MOV X10, 8(SP)
57+ MOV ·threadentry_call(SB), X5
58+ MOV (X5), X6
59+ JALR RA, 0(X6)
60+ RET
61+
62+TEXT ·call5(SB), NOSPLIT, $0-48
63+ MOV fn+0(FP), X9
64+ MOV a1+8(FP), X10
65+ MOV a2+16(FP), X11
66+ MOV a3+24(FP), X12
67+ MOV a4+32(FP), X13
68+ MOV a5+40(FP), X14
69+ JALR RA, 0(X9)
70+ MOV X10, ret+48(FP)
71+ RET
A struct_riscv64.go +143 -0
@@ -0,0 +1,143 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "reflect"
8+ "unsafe"
9+)
10+
11+func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
12+ outSize := outType.Size()
13+
14+ switch {
15+ case outSize == 0:
16+ return reflect.New(outType).Elem()
17+
18+ case outSize <= 16:
19+ // Reconstruct from registers by copying raw bytes
20+ var buf [16]byte
21+
22+ // Integer registers
23+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
24+ if outSize > 8 {
25+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
26+ }
27+
28+ // Homogeneous float aggregates override integer regs
29+ if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30+ if outType.Field(0).Type.Kind() == reflect.Float32 {
31+ // float32 values are NaN-boxed in FP regs; use low 32 bits only
32+ f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33+ for i := 0; i < numFields; i++ {
34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
35+ }
36+ } else {
37+ // float64: whole register value is valid
38+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
39+ if outSize > 8 {
40+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
41+ }
42+ }
43+ }
44+
45+ return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
46+
47+ default:
48+ // Returned indirectly via pointer in a1
49+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
50+ return reflect.NewAt(outType, ptr).Elem()
51+ }
52+}
53+
54+func addStruct(
55+ v reflect.Value,
56+ numInts, numFloats, numStack *int,
57+ addInt, addFloat, addStack func(uintptr),
58+ keepAlive []any,
59+) []any {
60+ size := v.Type().Size()
61+ if size == 0 {
62+ return keepAlive
63+ }
64+
65+ if size <= 16 {
66+ return placeSmallAggregateRISCV64(v, addFloat, addInt, keepAlive)
67+ }
68+
69+ return placeStack(v, keepAlive, addInt)
70+}
71+
72+func placeSmallAggregateRISCV64(
73+ v reflect.Value,
74+ addFloat, addInt func(uintptr),
75+ keepAlive []any,
76+) []any {
77+ size := v.Type().Size()
78+
79+ var ptr unsafe.Pointer
80+ if v.CanAddr() {
81+ ptr = v.Addr().UnsafePointer()
82+ } else {
83+ tmp := reflect.New(v.Type())
84+ tmp.Elem().Set(v)
85+ ptr = tmp.UnsafePointer()
86+ keepAlive = append(keepAlive, tmp.Interface())
87+ }
88+
89+ var buf [16]byte
90+ src := unsafe.Slice((*byte)(ptr), size)
91+ copy(buf[:], src)
92+
93+ w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
94+ w1 := uintptr(0)
95+ if size > 8 {
96+ w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
97+ }
98+
99+ if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
100+ addFloat(w0)
101+ if size > 8 {
102+ addFloat(w1)
103+ }
104+ } else {
105+ addInt(w0)
106+ if size > 8 {
107+ addInt(w1)
108+ }
109+ }
110+
111+ return keepAlive
112+}
113+
114+// placeStack is a fallback for structs that are too large to fit in registers
115+func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
116+ if v.CanAddr() {
117+ addInt(v.Addr().Pointer())
118+ return keepAlive
119+ }
120+ ptr := reflect.New(v.Type())
121+ ptr.Elem().Set(v)
122+ addInt(ptr.Pointer())
123+ return append(keepAlive, ptr.Interface())
124+}
125+
126+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
127+ // RISCV64 does not bundle stack args
128+ return false
129+}
130+
131+func collectStackArgs(
132+ args []reflect.Value,
133+ i, numInts, numFloats int,
134+ keepAlive []any,
135+ addInt, addFloat, addStack func(uintptr),
136+ numIntsPtr, numFloatsPtr, numStackPtr *int,
137+) ([]reflect.Value, []any) {
138+ return nil, keepAlive
139+}
140+
141+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
142+ panic("bundleStackArgs not supported on RISCV64")
143+}
@@ -0,0 +1,143 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "reflect"
8+ "unsafe"
9+)
10+
11+func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
12+ outSize := outType.Size()
13+
14+ switch {
15+ case outSize == 0:
16+ return reflect.New(outType).Elem()
17+
18+ case outSize <= 16:
19+ // Reconstruct from registers by copying raw bytes
20+ var buf [16]byte
21+
22+ // Integer registers
23+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
24+ if outSize > 8 {
25+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
26+ }
27+
28+ // Homogeneous float aggregates override integer regs
29+ if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30+ if outType.Field(0).Type.Kind() == reflect.Float32 {
31+ // float32 values are NaN-boxed in FP regs; use low 32 bits only
32+ f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33+ for i := 0; i < numFields; i++ {
34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
35+ }
36+ } else {
37+ // float64: whole register value is valid
38+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
39+ if outSize > 8 {
40+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
41+ }
42+ }
43+ }
44+
45+ return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
46+
47+ default:
48+ // Returned indirectly via pointer in a1
49+ ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
50+ return reflect.NewAt(outType, ptr).Elem()
51+ }
52+}
53+
54+func addStruct(
55+ v reflect.Value,
56+ numInts, numFloats, numStack *int,
57+ addInt, addFloat, addStack func(uintptr),
58+ keepAlive []any,
59+) []any {
60+ size := v.Type().Size()
61+ if size == 0 {
62+ return keepAlive
63+ }
64+
65+ if size <= 16 {
66+ return placeSmallAggregateRISCV64(v, addFloat, addInt, keepAlive)
67+ }
68+
69+ return placeStack(v, keepAlive, addInt)
70+}
71+
72+func placeSmallAggregateRISCV64(
73+ v reflect.Value,
74+ addFloat, addInt func(uintptr),
75+ keepAlive []any,
76+) []any {
77+ size := v.Type().Size()
78+
79+ var ptr unsafe.Pointer
80+ if v.CanAddr() {
81+ ptr = v.Addr().UnsafePointer()
82+ } else {
83+ tmp := reflect.New(v.Type())
84+ tmp.Elem().Set(v)
85+ ptr = tmp.UnsafePointer()
86+ keepAlive = append(keepAlive, tmp.Interface())
87+ }
88+
89+ var buf [16]byte
90+ src := unsafe.Slice((*byte)(ptr), size)
91+ copy(buf[:], src)
92+
93+ w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
94+ w1 := uintptr(0)
95+ if size > 8 {
96+ w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
97+ }
98+
99+ if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
100+ addFloat(w0)
101+ if size > 8 {
102+ addFloat(w1)
103+ }
104+ } else {
105+ addInt(w0)
106+ if size > 8 {
107+ addInt(w1)
108+ }
109+ }
110+
111+ return keepAlive
112+}
113+
114+// placeStack is a fallback for structs that are too large to fit in registers
115+func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
116+ if v.CanAddr() {
117+ addInt(v.Addr().Pointer())
118+ return keepAlive
119+ }
120+ ptr := reflect.New(v.Type())
121+ ptr.Elem().Set(v)
122+ addInt(ptr.Pointer())
123+ return append(keepAlive, ptr.Interface())
124+}
125+
126+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
127+ // RISCV64 does not bundle stack args
128+ return false
129+}
130+
131+func collectStackArgs(
132+ args []reflect.Value,
133+ i, numInts, numFloats int,
134+ keepAlive []any,
135+ addInt, addFloat, addStack func(uintptr),
136+ numIntsPtr, numFloatsPtr, numStackPtr *int,
137+) ([]reflect.Value, []any) {
138+ return nil, keepAlive
139+}
140+
141+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
142+ panic("bundleStackArgs not supported on RISCV64")
143+}
A sys_riscv64.s +101 -0
@@ -0,0 +1,101 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// Stack usage:
11+// 0(SP) - 56(SP): stack args a9-a15 (7 * 8 bytes = 56)
12+// 56(SP) - 64(SP): saved RA (x1)
13+// 64(SP) - 72(SP): saved X9 (s1)
14+// 72(SP) - 80(SP): saved X18 (s2)
15+// 80(SP) - 88(SP): saved args pointer (original X10)
16+// 88(SP) - 96(SP): padding
17+#define STACK_SIZE 96
18+#define SAVE_RA 56
19+#define SAVE_X9 64
20+#define SAVE_X18 72
21+#define SAVE_ARGP 80
22+
23+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
24+DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
25+
26+TEXT syscall15X(SB), NOSPLIT, $0
27+ // Allocate stack frame (keeps 16-byte alignment)
28+ SUB $STACK_SIZE, SP
29+
30+ // Save callee-saved regs we clobber + return address
31+ MOV X1, SAVE_RA(SP)
32+ MOV X9, SAVE_X9(SP)
33+ MOV X18, SAVE_X18(SP)
34+
35+ // Save original args pointer (in a0/X10)
36+ MOV X10, SAVE_ARGP(SP)
37+
38+ // X9 := args pointer (syscall15Args*)
39+ MOV X10, X9
40+
41+ // Load float args into fa0-fa7 (F10-F17)
42+ MOVD syscall15Args_f1(X9), F10
43+ MOVD syscall15Args_f2(X9), F11
44+ MOVD syscall15Args_f3(X9), F12
45+ MOVD syscall15Args_f4(X9), F13
46+ MOVD syscall15Args_f5(X9), F14
47+ MOVD syscall15Args_f6(X9), F15
48+ MOVD syscall15Args_f7(X9), F16
49+ MOVD syscall15Args_f8(X9), F17
50+
51+ // Load integer args into a0-a7 (X10-X17)
52+ MOV syscall15Args_a1(X9), X10
53+ MOV syscall15Args_a2(X9), X11
54+ MOV syscall15Args_a3(X9), X12
55+ MOV syscall15Args_a4(X9), X13
56+ MOV syscall15Args_a5(X9), X14
57+ MOV syscall15Args_a6(X9), X15
58+ MOV syscall15Args_a7(X9), X16
59+ MOV syscall15Args_a8(X9), X17
60+
61+ // Spill a9-a15 onto the stack (C ABI)
62+ MOV syscall15Args_a9(X9), X18
63+ MOV X18, 0(SP)
64+ MOV syscall15Args_a10(X9), X18
65+ MOV X18, 8(SP)
66+ MOV syscall15Args_a11(X9), X18
67+ MOV X18, 16(SP)
68+ MOV syscall15Args_a12(X9), X18
69+ MOV X18, 24(SP)
70+ MOV syscall15Args_a13(X9), X18
71+ MOV X18, 32(SP)
72+ MOV syscall15Args_a14(X9), X18
73+ MOV X18, 40(SP)
74+ MOV syscall15Args_a15(X9), X18
75+ MOV X18, 48(SP)
76+
77+ // Call fn
78+ // IMPORTANT: preserve RA across this call (we saved it above)
79+ MOV syscall15Args_fn(X9), X18
80+ JALR RA, X18
81+
82+ // Restore args pointer (syscall15Args*) for storing results
83+ MOV SAVE_ARGP(SP), X9
84+
85+ // Store results back
86+ MOV X10, syscall15Args_a1(X9)
87+ MOV X11, syscall15Args_a2(X9)
88+
89+ // Store back float return regs if used by your ABI contract
90+ MOVD F10, syscall15Args_f1(X9)
91+ MOVD F11, syscall15Args_f2(X9)
92+ MOVD F12, syscall15Args_f3(X9)
93+ MOVD F13, syscall15Args_f4(X9)
94+
95+ // Restore callee-saved regs and return address
96+ MOV SAVE_X18(SP), X18
97+ MOV SAVE_X9(SP), X9
98+ MOV SAVE_RA(SP), X1
99+
100+ ADD $STACK_SIZE, SP
101+ RET
@@ -0,0 +1,101 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+// Stack usage:
11+// 0(SP) - 56(SP): stack args a9-a15 (7 * 8 bytes = 56)
12+// 56(SP) - 64(SP): saved RA (x1)
13+// 64(SP) - 72(SP): saved X9 (s1)
14+// 72(SP) - 80(SP): saved X18 (s2)
15+// 80(SP) - 88(SP): saved args pointer (original X10)
16+// 88(SP) - 96(SP): padding
17+#define STACK_SIZE 96
18+#define SAVE_RA 56
19+#define SAVE_X9 64
20+#define SAVE_X18 72
21+#define SAVE_ARGP 80
22+
23+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
24+DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
25+
26+TEXT syscall15X(SB), NOSPLIT, $0
27+ // Allocate stack frame (keeps 16-byte alignment)
28+ SUB $STACK_SIZE, SP
29+
30+ // Save callee-saved regs we clobber + return address
31+ MOV X1, SAVE_RA(SP)
32+ MOV X9, SAVE_X9(SP)
33+ MOV X18, SAVE_X18(SP)
34+
35+ // Save original args pointer (in a0/X10)
36+ MOV X10, SAVE_ARGP(SP)
37+
38+ // X9 := args pointer (syscall15Args*)
39+ MOV X10, X9
40+
41+ // Load float args into fa0-fa7 (F10-F17)
42+ MOVD syscall15Args_f1(X9), F10
43+ MOVD syscall15Args_f2(X9), F11
44+ MOVD syscall15Args_f3(X9), F12
45+ MOVD syscall15Args_f4(X9), F13
46+ MOVD syscall15Args_f5(X9), F14
47+ MOVD syscall15Args_f6(X9), F15
48+ MOVD syscall15Args_f7(X9), F16
49+ MOVD syscall15Args_f8(X9), F17
50+
51+ // Load integer args into a0-a7 (X10-X17)
52+ MOV syscall15Args_a1(X9), X10
53+ MOV syscall15Args_a2(X9), X11
54+ MOV syscall15Args_a3(X9), X12
55+ MOV syscall15Args_a4(X9), X13
56+ MOV syscall15Args_a5(X9), X14
57+ MOV syscall15Args_a6(X9), X15
58+ MOV syscall15Args_a7(X9), X16
59+ MOV syscall15Args_a8(X9), X17
60+
61+ // Spill a9-a15 onto the stack (C ABI)
62+ MOV syscall15Args_a9(X9), X18
63+ MOV X18, 0(SP)
64+ MOV syscall15Args_a10(X9), X18
65+ MOV X18, 8(SP)
66+ MOV syscall15Args_a11(X9), X18
67+ MOV X18, 16(SP)
68+ MOV syscall15Args_a12(X9), X18
69+ MOV X18, 24(SP)
70+ MOV syscall15Args_a13(X9), X18
71+ MOV X18, 32(SP)
72+ MOV syscall15Args_a14(X9), X18
73+ MOV X18, 40(SP)
74+ MOV syscall15Args_a15(X9), X18
75+ MOV X18, 48(SP)
76+
77+ // Call fn
78+ // IMPORTANT: preserve RA across this call (we saved it above)
79+ MOV syscall15Args_fn(X9), X18
80+ JALR RA, X18
81+
82+ // Restore args pointer (syscall15Args*) for storing results
83+ MOV SAVE_ARGP(SP), X9
84+
85+ // Store results back
86+ MOV X10, syscall15Args_a1(X9)
87+ MOV X11, syscall15Args_a2(X9)
88+
89+ // Store back float return regs if used by your ABI contract
90+ MOVD F10, syscall15Args_f1(X9)
91+ MOVD F11, syscall15Args_f2(X9)
92+ MOVD F12, syscall15Args_f3(X9)
93+ MOVD F13, syscall15Args_f4(X9)
94+
95+ // Restore callee-saved regs and return address
96+ MOV SAVE_X18(SP), X18
97+ MOV SAVE_X9(SP), X9
98+ MOV SAVE_RA(SP), X1
99+
100+ ADD $STACK_SIZE, SP
101+ RET
A sys_unix_riscv64.s +79 -0
@@ -0,0 +1,79 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+TEXT callbackasm1(SB), NOFRAME, $0
11+ NO_LOCAL_POINTERS
12+
13+ // On entry, the trampoline in zcallback_riscv64.s left
14+ // the callback index in X7.
15+
16+ // Save callback register arguments X10-X17 and F10-F17.
17+ // Stack args (if any) are at 0(SP), 8(SP), etc.
18+ // We save register args at SP-128, making them contiguous with stack args.
19+ ADD $-(16*8), SP, X6
20+
21+ // Save float arg regs fa0-fa7 (F10-F17)
22+ MOVD F10, 0(X6)
23+ MOVD F11, 8(X6)
24+ MOVD F12, 16(X6)
25+ MOVD F13, 24(X6)
26+ MOVD F14, 32(X6)
27+ MOVD F15, 40(X6)
28+ MOVD F16, 48(X6)
29+ MOVD F17, 56(X6)
30+
31+ // Save integer arg regs a0-a7 (X10-X17)
32+ MOV X10, 64(X6)
33+ MOV X11, 72(X6)
34+ MOV X12, 80(X6)
35+ MOV X13, 88(X6)
36+ MOV X14, 96(X6)
37+ MOV X15, 104(X6)
38+ MOV X16, 112(X6)
39+ MOV X17, 120(X6)
40+
41+ // Allocate space on stack for RA, saved regs, and callbackArgs.
42+ // We need: 8 (RA) + 8 (X9 callee-saved) + 24 (callbackArgs) = 40, round to 176 (22*8)
43+ // to match loong64 and ensure we don't overlap with saved register args.
44+ // The saved regs end at SP-8 (original), so we need new SP below SP-128.
45+ ADD $-(22*8), SP
46+
47+ // Save link register (RA/X1) and callee-saved register X9
48+ // (X9 is used by the assembler for some instructions)
49+ MOV X1, 0(SP)
50+ MOV X9, 8(SP)
51+
52+ // Create a struct callbackArgs on our stack.
53+ // callbackArgs struct: index(0), args(8), result(16)
54+ // Place it at 16(SP) to avoid overlap
55+ ADD $16, SP, X9
56+ MOV X7, 0(X9) // callback index
57+ MOV X6, 8(X9) // address of args vector
58+ MOV X0, 16(X9) // result = 0
59+
60+ // Call crosscall2 with arguments in registers
61+ MOV ·callbackWrap_call(SB), X10 // Get the ABIInternal function pointer
62+ MOV (X10), X10 // without <ABIInternal> by using a closure. X10 = fn
63+ MOV X9, X11 // X11 = frame (address of callbackArgs)
64+ MOV X0, X13 // X13 = ctxt = 0
65+
66+ CALL crosscall2(SB)
67+
68+ // Get callback result.
69+ ADD $16, SP, X9
70+ MOV 16(X9), X10
71+
72+ // Restore link register and callee-saved X9
73+ MOV 8(SP), X9
74+ MOV 0(SP), X1
75+
76+ // Restore stack pointer
77+ ADD $(22*8), SP
78+
79+ RET
@@ -0,0 +1,79 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+TEXT callbackasm1(SB), NOFRAME, $0
11+ NO_LOCAL_POINTERS
12+
13+ // On entry, the trampoline in zcallback_riscv64.s left
14+ // the callback index in X7.
15+
16+ // Save callback register arguments X10-X17 and F10-F17.
17+ // Stack args (if any) are at 0(SP), 8(SP), etc.
18+ // We save register args at SP-128, making them contiguous with stack args.
19+ ADD $-(16*8), SP, X6
20+
21+ // Save float arg regs fa0-fa7 (F10-F17)
22+ MOVD F10, 0(X6)
23+ MOVD F11, 8(X6)
24+ MOVD F12, 16(X6)
25+ MOVD F13, 24(X6)
26+ MOVD F14, 32(X6)
27+ MOVD F15, 40(X6)
28+ MOVD F16, 48(X6)
29+ MOVD F17, 56(X6)
30+
31+ // Save integer arg regs a0-a7 (X10-X17)
32+ MOV X10, 64(X6)
33+ MOV X11, 72(X6)
34+ MOV X12, 80(X6)
35+ MOV X13, 88(X6)
36+ MOV X14, 96(X6)
37+ MOV X15, 104(X6)
38+ MOV X16, 112(X6)
39+ MOV X17, 120(X6)
40+
41+ // Allocate space on stack for RA, saved regs, and callbackArgs.
42+ // We need: 8 (RA) + 8 (X9 callee-saved) + 24 (callbackArgs) = 40, round to 176 (22*8)
43+ // to match loong64 and ensure we don't overlap with saved register args.
44+ // The saved regs end at SP-8 (original), so we need new SP below SP-128.
45+ ADD $-(22*8), SP
46+
47+ // Save link register (RA/X1) and callee-saved register X9
48+ // (X9 is used by the assembler for some instructions)
49+ MOV X1, 0(SP)
50+ MOV X9, 8(SP)
51+
52+ // Create a struct callbackArgs on our stack.
53+ // callbackArgs struct: index(0), args(8), result(16)
54+ // Place it at 16(SP) to avoid overlap
55+ ADD $16, SP, X9
56+ MOV X7, 0(X9) // callback index
57+ MOV X6, 8(X9) // address of args vector
58+ MOV X0, 16(X9) // result = 0
59+
60+ // Call crosscall2 with arguments in registers
61+ MOV ·callbackWrap_call(SB), X10 // Get the ABIInternal function pointer
62+ MOV (X10), X10 // without <ABIInternal> by using a closure. X10 = fn
63+ MOV X9, X11 // X11 = frame (address of callbackArgs)
64+ MOV X0, X13 // X13 = ctxt = 0
65+
66+ CALL crosscall2(SB)
67+
68+ // Get callback result.
69+ ADD $16, SP, X9
70+ MOV 16(X9), X10
71+
72+ // Restore link register and callee-saved X9
73+ MOV 8(SP), X9
74+ MOV 0(SP), X1
75+
76+ // Restore stack pointer
77+ ADD $(22*8), SP
78+
79+ RET
M syscall_cgo_linux.go +2 -2
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build cgo && !(amd64 || arm64 || loong64)
4+//go:build cgo && !(amd64 || arm64 || loong64 || riscv64)
55
66 package purego
77
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
1717 }
1818
1919 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64")
20+ panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64/riscv64")
2121 }
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build cgo && !(amd64 || arm64 || loong64)4+//go:build cgo && !(amd64 || arm64 || loong64 || riscv64)
5 5
6 package purego6 package purego
7 7
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
17 }17 }
18 18
19 func NewCallback(_ any) uintptr {19 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64")20+ panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64/riscv64")
21 }21 }
M syscall_other.go +1 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors
33
4-//go:build linux && !amd64 && !arm64 && !loong64
4+//go:build linux && !amd64 && !arm64 && !loong64 && !riscv64
55
66 package purego
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3 3
4-//go:build linux && !amd64 && !arm64 && !loong644+//go:build linux && !amd64 && !arm64 && !loong64 && !riscv64
5 5
6 package purego6 package purego
7 7
M syscall_sysv.go +3 -3
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd
4+//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd
55
66 package purego
77
@@ -217,8 +217,8 @@ func callbackasmAddr(i int) uintptr {
217217 panic("purego: unsupported architecture")
218218 case "386", "amd64":
219219 entrySize = 5
220- case "arm", "arm64", "loong64":
221- // On ARM and ARM64, each entry is a MOV instruction
220+ case "arm", "arm64", "loong64", "riscv64":
221+ // On ARM, ARM64, Loong64, and RISCV64, each entry is a MOV instruction
222222 // followed by a branch instruction
223223 entrySize = 8
224224 }
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd4+//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd
5 5
6 package purego6 package purego
7 7
@@ -217,8 +217,8 @@ func callbackasmAddr(i int) uintptr {
217 panic("purego: unsupported architecture")217 panic("purego: unsupported architecture")
218 case "386", "amd64":218 case "386", "amd64":
219 entrySize = 5219 entrySize = 5
220- case "arm", "arm64", "loong64":220+ case "arm", "arm64", "loong64", "riscv64":
221- // On ARM and ARM64, each entry is a MOV instruction221+ // On ARM, ARM64, Loong64, and RISCV64, each entry is a MOV instruction
222 // followed by a branch instruction222 // followed by a branch instruction
223 entrySize = 8223 entrySize = 8
224 }224 }
M wincallback.go +30 -0
@@ -100,8 +100,38 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
100100 }
101101 }
102102
103+func genasmRiscv64() {
104+ var buf bytes.Buffer
105+
106+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
107+
108+//go:build darwin || freebsd || linux || netbsd
109+
110+// External code calls into callbackasm at an offset corresponding
111+// to the callback index. Callbackasm is a table of MOV and JMP instructions.
112+// The MOV instruction loads X7 with the callback index, and the
113+// JMP instruction branches to callbackasm1.
114+// callbackasm1 takes the callback index from X7 and
115+// indexes into an array that stores information about each callback.
116+// It then calls the Go implementation for that callback.
117+#include "textflag.h"
118+
119+TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
120+`)
121+ for i := 0; i < maxCallback; i++ {
122+ fmt.Fprintf(&buf, "\tPCALIGN\t$8\n")
123+ fmt.Fprintf(&buf, "\tMOV\t$%d, X7\n", i)
124+ buf.WriteString("\tJMP\tcallbackasm1(SB)\n")
125+ }
126+ if err := os.WriteFile("zcallback_riscv64.s", buf.Bytes(), 0644); err != nil {
127+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
128+ os.Exit(2)
129+ }
130+}
131+
103132 func main() {
104133 genasmAmd64()
105134 genasmArm64()
106135 genasmLoong64()
136+ genasmRiscv64()
107137 }
@@ -100,8 +100,38 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
100 }100 }
101 }101 }
102 102
103+func genasmRiscv64() {
104+ var buf bytes.Buffer
105+
106+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
107+
108+//go:build darwin || freebsd || linux || netbsd
109+
110+// External code calls into callbackasm at an offset corresponding
111+// to the callback index. Callbackasm is a table of MOV and JMP instructions.
112+// The MOV instruction loads X7 with the callback index, and the
113+// JMP instruction branches to callbackasm1.
114+// callbackasm1 takes the callback index from X7 and
115+// indexes into an array that stores information about each callback.
116+// It then calls the Go implementation for that callback.
117+#include "textflag.h"
118+
119+TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
120+`)
121+ for i := 0; i < maxCallback; i++ {
122+ fmt.Fprintf(&buf, "\tPCALIGN\t$8\n")
123+ fmt.Fprintf(&buf, "\tMOV\t$%d, X7\n", i)
124+ buf.WriteString("\tJMP\tcallbackasm1(SB)\n")
125+ }
126+ if err := os.WriteFile("zcallback_riscv64.s", buf.Bytes(), 0644); err != nil {
127+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
128+ os.Exit(2)
129+ }
130+}
131+
103 func main() {132 func main() {
104 genasmAmd64()133 genasmAmd64()
105 genasmArm64()134 genasmArm64()
106 genasmLoong64()135 genasmLoong64()
136+ genasmRiscv64()
107 }137 }
A zcallback_riscv64.s +6014 -0 large diff (6014 lines changed): collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close