v0.11.0-alpha.8 d476432

func.go RawHistoryFolder

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3
4//go:build darwin || freebsd || linux || netbsd || windows
5
6package purego
7
8import (
9	"fmt"
10	"math"
11	"reflect"
12	"runtime"
13	"sync"
14	"unsafe"
15
16	"github.com/ebitengine/purego/internal/strings"
17)
18
19const (
20	align8ByteMask = 7 // Mask for 8-byte alignment: (val + 7) &^ 7
21	align8ByteSize = 8 // 8-byte alignment boundary
22)
23
24var thePool = sync.Pool{New: func() any {
25	return new(syscallArgs)
26}}
27
28// RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name).
29// It panics if it can't find the name symbol.
30func RegisterLibFunc(fptr any, handle uintptr, name string) {
31	sym, err := loadSymbol(handle, name)
32	if err != nil {
33		panic(err)
34	}
35	RegisterFunc(fptr, sym)
36}
37
38// RegisterFunc takes a pointer to a Go function representing the calling convention of the C function.
39// fptr will be set to a function that when called will call the C function given by cfn with the
40// parameters passed in the correct registers and stack.
41//
42// A panic is produced if the type is not a function pointer or if the function returns more than 1 value.
43//
44// These conversions describe how a Go type in the fptr will be used to call
45// the C function. It is important to note that there is no way to verify that fptr
46// matches the C function. This also holds true for struct types where the padding
47// needs to be ensured to match that of C; RegisterFunc does not verify this.
48//
49// # Type Conversions (Go <=> C)
50//
51//	string <=> char*
52//	bool <=> _Bool
53//	uintptr <=> uintptr_t
54//	uint <=> uint32_t or uint64_t
55//	uint8 <=> uint8_t
56//	uint16 <=> uint16_t
57//	uint32 <=> uint32_t
58//	uint64 <=> uint64_t
59//	int <=> int32_t or int64_t
60//	int8 <=> int8_t
61//	int16 <=> int16_t
62//	int32 <=> int32_t
63//	int64 <=> int64_t
64//	float32 <=> float
65//	float64 <=> double
66//	struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64)
67//	func <=> C function
68//	unsafe.Pointer, *T <=> void*
69//	[]T => void*
70//
71// There is a special case when the last argument of fptr is a variadic interface (or []interface}
72// it will be expanded into a call to the C function as if it had the arguments in that slice.
73// This means that using arg ...any is like a cast to the function with the arguments inside arg.
74// This is not the same as C variadic.
75//
76// # Memory
77//
78// In general it is not possible for purego to guarantee the lifetimes of objects returned or received from
79// calling functions using RegisterFunc. For arguments to a C function it is important that the C function doesn't
80// hold onto a reference to Go memory. This is the same as the [Cgo rules].
81//
82// However, there are some special cases. When passing a string as an argument if the string does not end in a null
83// terminated byte (\x00) then the string will be copied into memory maintained by purego. The memory is only valid for
84// that specific call. Therefore, if the C code keeps a reference to that string it may become invalid at some
85// undefined time. However, if the string does already contain a null-terminated byte then no copy is done.
86// It is then the responsibility of the caller to ensure the string stays alive as long as it's needed in C memory.
87// This can be done using runtime.KeepAlive or allocating the string in C memory using malloc. When a C function
88// returns a null-terminated pointer to char a Go string can be used. Purego will allocate a new string in Go memory
89// and copy the data over. This string will be garbage collected whenever Go decides it's no longer referenced.
90// This C created string will not be freed by purego. If the pointer to char is not null-terminated or must continue
91// to point to C memory (because it's a buffer for example) then use a pointer to byte and then convert that to a slice
92// using unsafe.Slice. Doing this means that it becomes the responsibility of the caller to care about the lifetime
93// of the pointer
94//
95// # Structs
96//
97// Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However,
98// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
99// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
100//
101// On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments
102// when passing them on the stack, following the C ABI's byte-level packing rules.
103//
104// On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.
105// Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.
106//
107// # Example
108//
109// All functions below call this C function:
110//
111//	char *foo(char *str);
112//
113//	// Let purego convert types
114//	var foo func(s string) string
115//	goString := foo("copied")
116//	// Go will garbage collect this string
117//
118//	// Manually, handle allocations
119//	var foo2 func(b string) *byte
120//	mustFree := foo2("not copied\x00")
121//	defer free(mustFree)
122//
123// [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
124func RegisterFunc(fptr any, cfn uintptr) {
125	const is32bit = unsafe.Sizeof(uintptr(0)) == 4
126	fn := reflect.ValueOf(fptr).Elem()
127	ty := fn.Type()
128	if ty.Kind() != reflect.Func {
129		panic("purego: fptr must be a function pointer")
130	}
131	if ty.NumOut() > 1 {
132		panic("purego: function can only return zero or one values")
133	}
134	if cfn == 0 {
135		panic("purego: cfn is nil")
136	}
137	if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
138		runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
139		panic("purego: float returns are not supported")
140	}
141	{
142		// this code checks how many registers and stack this function will use
143		// to avoid crashing with too many arguments
144		var ints int
145		var floats int
146		floatArgRegs := numOfFloatRegisters()
147		var stack int
148		for i := 0; i < ty.NumIn(); i++ {
149			arg := ty.In(i)
150			switch arg.Kind() {
151			case reflect.Func:
152				// This only does preliminary testing to ensure the CDecl argument
153				// is the first argument. Full testing is done when the callback is actually
154				// created in NewCallback.
155				for j := 0; j < arg.NumIn(); j++ {
156					in := arg.In(j)
157					if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
158						continue
159					}
160					if j != 0 {
161						panic("purego: CDecl must be the first argument")
162					}
163				}
164			case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
165				reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer,
166				reflect.Slice, reflect.Bool:
167				if ints < numOfIntegerRegisters() {
168					ints++
169				} else {
170					stack++
171				}
172			case reflect.Float32, reflect.Float64:
173				if floats < floatArgRegs {
174					floats++
175				} else {
176					stack++
177				}
178			case reflect.Struct:
179				ensureStructSupported()
180				if arg.Size() == 0 && runtime.GOOS != "windows" {
181					// On Windows an empty struct still consumes one argument slot.
182					continue
183				}
184				addInt := func(u uintptr) {
185					ints++
186				}
187				addFloat := func(u uintptr) {
188					floats++
189				}
190				addStack := func(u uintptr) {
191					stack++
192				}
193				_ = addStruct(reflect.New(arg).Elem(), &ints, &floats, &stack, addInt, addFloat, addStack, nil)
194			default:
195				panic("purego: unsupported kind " + arg.Kind().String())
196			}
197		}
198		if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
199			ensureStructSupported()
200			outType := ty.Out(0)
201			checkStructFieldsSupported(outType)
202			if structReturnInMemory(outType) {
203				// A struct returned in memory is allocated by the caller and its
204				// pointer is passed as a hidden first integer argument. When the
205				// integer registers are already full, prepending it spills a
206				// regular argument onto the stack.
207				if ints < numOfIntegerRegisters() {
208					ints++
209				} else {
210					stack++
211				}
212			}
213		}
214
215		argsLimit := maxArgs
216		sizeOfStack := argsLimit - numOfIntegerRegisters()
217		if runtime.GOOS == "windows" {
218			if ints+floats+stack > argsLimit {
219				panic("purego: too many stack arguments")
220			}
221		} else if isDarwin && runtime.GOARCH == "arm64" {
222			// On Darwin ARM64, use byte-based validation since arguments pack efficiently.
223			// See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
224			stackBytes := estimateStackBytes(ty)
225			maxStackBytes := sizeOfStack * 8
226			if stackBytes > maxStackBytes {
227				panic("purego: too many stack arguments")
228			}
229		} else {
230			if stack > sizeOfStack {
231				panic("purego: too many stack arguments")
232			}
233		}
234	}
235
236	v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
237		var sysargs [maxArgs]uintptr
238		// Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
239		// since numOfFloatRegisters() is a function call, not a constant.
240		// maxArgs is always greater than or equal to numOfFloatRegisters() so this is safe.
241		var floats [maxArgs]uintptr
242		floatArgRegs := numOfFloatRegisters()
243		var numInts int
244		var numFloats int
245		var numStack int
246		var addStack, addInt, addFloat func(x uintptr)
247		if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
248			// Windows arm64 uses the same calling convention as macOS and Linux
249			addStack = func(x uintptr) {
250				sysargs[numOfIntegerRegisters()+numStack] = x
251				numStack++
252			}
253			addInt = func(x uintptr) {
254				if numInts >= numOfIntegerRegisters() {
255					addStack(x)
256				} else {
257					sysargs[numInts] = x
258					numInts++
259				}
260			}
261			addFloat = func(x uintptr) {
262				if numFloats < floatArgRegs {
263					floats[numFloats] = x
264					numFloats++
265				} else {
266					addStack(x)
267				}
268			}
269		} else {
270			// On Windows amd64 the arguments are passed in the numbered registered.
271			// So the first int is in the first integer register and the first float
272			// is in the second floating register if there is already a first int.
273			// This is in contrast to how macOS and Linux pass arguments which
274			// tries to use as many registers as possible in the calling convention.
275			addStack = func(x uintptr) {
276				if numStack >= maxArgs {
277					panic("purego: too many stack arguments")
278				}
279				sysargs[numStack] = x
280				numStack++
281			}
282			addInt = addStack
283			addFloat = addStack
284		}
285
286		var keepAlive []any
287		defer func() {
288			runtime.KeepAlive(keepAlive)
289			runtime.KeepAlive(args)
290		}()
291
292		var arm64_r8 uintptr
293		if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
294			outType := ty.Out(0)
295			if structReturnInMemory(outType) {
296				// The caller allocates the return value and passes its pointer
297				// as a hidden first integer argument.
298				val := reflect.New(outType)
299				keepAlive = append(keepAlive, val)
300				addInt(val.Pointer())
301			} else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
302				isAllFloats, numFields := isAllSameFloat(outType)
303				if !isAllFloats || numFields > 4 {
304					val := reflect.New(outType)
305					keepAlive = append(keepAlive, val)
306					arm64_r8 = val.Pointer()
307				}
308			}
309		}
310		for i, v := range args {
311			if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok {
312				if i != len(args)-1 {
313					panic("purego: can only expand last parameter")
314				}
315				for _, x := range variadic {
316					keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
317				}
318				continue
319			}
320			// Check if we need to start Darwin ARM64 C-style stack packing
321			if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) {
322				// Collect and separate remaining args into register vs stack
323				stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
324					keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
325				keepAlive = newKeepAlive
326
327				// Bundle stack arguments with C-style packing
328				bundleStackArgs(stackArgs, addStack)
329				break
330			}
331			keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
332		}
333
334		var syscall *syscallArgs
335		if runtime.GOOS == "windows" && runtime.GOARCH != "arm64" {
336			// Windows amd64, 386, and arm use syscall.SyscallN.
337			syscall = thePool.Get().(*syscallArgs)
338			syscall.a1, syscall.a2, _ = syscall_syscallN(cfn, sysargs[:numStack]...)
339			syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
340		} else {
341			syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], arm64_r8)
342		}
343		defer thePool.Put(syscall)
344		if ty.NumOut() == 0 {
345			return nil
346		}
347		outType := ty.Out(0)
348		v := reflect.New(outType).Elem()
349		switch outType.Kind() {
350		case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
351			v.SetUint(uint64(syscall.a1))
352		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
353			v.SetInt(int64(syscall.a1))
354		case reflect.Bool:
355			v.SetBool(byte(syscall.a1) != 0)
356		case reflect.UnsafePointer:
357			// We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
358			v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
359		case reflect.Pointer:
360			// Copy syscall.a1 into a local variable to prevent v
361			// from holding a pointer to the pooled syscallArgs field.
362			a1 := syscall.a1
363			v = reflect.NewAt(outType, unsafe.Pointer(&a1)).Elem()
364		case reflect.Func:
365			// wrap this C function in a nicely typed Go function
366			v = reflect.New(outType)
367			RegisterFunc(v.Interface(), syscall.a1)
368		case reflect.String:
369			v.SetString(strings.GoString(syscall.a1))
370		case reflect.Float32:
371			// NOTE: syscall.r2 is only the floating return value on 64bit platforms.
372			// On 32bit platforms syscall.r2 is the upper part of a 64bit return.
373			// On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
374			// On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
375			// On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
376			switch runtime.GOARCH {
377			case "386":
378				v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
379			case "ppc64le":
380				v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
381			case "s390x":
382				// S390X is big-endian: float32 in upper 32 bits of 64-bit register
383				v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
384			default:
385				v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
386			}
387		case reflect.Float64:
388			// NOTE: syscall.r2 is only the floating return value on 64bit platforms.
389			// On 32bit platforms syscall.r2 is the upper part of a 64bit return.
390			if is32bit {
391				v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
392			} else {
393				v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
394			}
395		case reflect.Struct:
396			v = getStruct(outType, *syscall)
397		default:
398			panic("purego: unsupported return kind: " + outType.Kind().String())
399		}
400		if len(args) > 0 {
401			// reuse args slice instead of allocating one when possible
402			args[0] = v
403			return args[:1]
404		} else {
405			return []reflect.Value{v}
406		}
407	})
408	fn.Set(v)
409}
410
411func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
412	const is32bit = unsafe.Sizeof(uintptr(0)) == 4
413	switch v.Kind() {
414	case reflect.String:
415		ptr := strings.CString(v.String())
416		keepAlive = append(keepAlive, ptr)
417		addInt(uintptr(unsafe.Pointer(ptr)))
418	case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
419		addInt(uintptr(v.Uint()))
420	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
421		addInt(uintptr(v.Int()))
422	case reflect.Pointer, reflect.UnsafePointer, reflect.Slice:
423		// There is no need to keepAlive this pointer separately because it is kept alive in the args variable
424		addInt(v.Pointer())
425	case reflect.Func:
426		addInt(NewCallback(v.Interface()))
427	case reflect.Bool:
428		if v.Bool() {
429			addInt(1)
430		} else {
431			addInt(0)
432		}
433	case reflect.Float32:
434		switch runtime.GOARCH {
435		case "ppc64le":
436			// A single-precision argument occupies a floating-point register in double format on Power.
437			addFloat(uintptr(math.Float64bits(v.Float())))
438		case "s390x":
439			// S390X big-endian: float32 goes in the upper 32 bits of the 64-bit FP register.
440			addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
441		default:
442			addFloat(uintptr(math.Float32bits(float32(v.Float()))))
443		}
444	case reflect.Float64:
445		if is32bit {
446			bits := math.Float64bits(v.Float())
447			addFloat(uintptr(bits))
448			addFloat(uintptr(bits >> 32))
449		} else {
450			addFloat(uintptr(math.Float64bits(v.Float())))
451		}
452	case reflect.Struct:
453		keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
454	default:
455		panic("purego: unsupported kind: " + v.Kind().String())
456	}
457	return keepAlive
458}
459
460// maxRegAllocStructSize is the biggest a struct can be while still fitting in registers.
461// if it is bigger than this than enough space must be allocated on the heap and then passed into
462// the function as the first parameter on amd64 or in R8 on arm64.
463//
464// If you change this make sure to update it in objc_runtime_darwin.go
465const maxRegAllocStructSize = 16
466
467func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
468	allFloats = true
469	root := ty.Field(0).Type
470	for root.Kind() == reflect.Struct {
471		root = root.Field(0).Type
472	}
473	first := root.Kind()
474	if first != reflect.Float32 && first != reflect.Float64 {
475		allFloats = false
476	}
477	for i := 0; i < ty.NumField(); i++ {
478		f := ty.Field(i).Type
479		if f.Kind() == reflect.Struct {
480			var structNumFields int
481			allFloats, structNumFields = isAllSameFloat(f)
482			numFields += structNumFields
483			continue
484		}
485		numFields++
486		if f.Kind() != first {
487			allFloats = false
488		}
489	}
490	return allFloats, numFields
491}
492
493func checkStructFieldsSupported(ty reflect.Type) {
494	for i := 0; i < ty.NumField(); i++ {
495		f := ty.Field(i).Type
496		if f.Kind() == reflect.Array {
497			f = f.Elem()
498		} else if f.Kind() == reflect.Struct {
499			checkStructFieldsSupported(f)
500			continue
501		}
502		switch f.Kind() {
503		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
504			reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
505			reflect.Uintptr, reflect.Pointer, reflect.UnsafePointer, reflect.Float64, reflect.Float32,
506			reflect.Bool:
507		default:
508			panic(fmt.Sprintf("purego: struct field type %s is not supported", f))
509		}
510	}
511}
512
513// ensureStructSupported panics if passing or returning structs through a call to
514// a C function is unsupported on the current platform.
515func ensureStructSupported() {
516	switch runtime.GOARCH {
517	case "amd64", "arm64", "loong64", "ppc64le":
518	default:
519		panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, and ppc64le")
520	}
521	switch runtime.GOOS {
522	case "android", "darwin", "ios", "linux", "windows":
523	default:
524		panic("purego: struct arguments/returns are only supported on android, darwin, ios, linux, and windows")
525	}
526}
527
528// ensureCallbackStructSupported panics if passing or returning structs through a
529// callback is unsupported on the current platform. Callbacks support structs on
530// fewer architectures than a direct call to a C function.
531func ensureCallbackStructSupported() {
532	if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
533		panic("purego: struct arguments/returns in callbacks are only supported on amd64 and arm64")
534	}
535	switch runtime.GOOS {
536	case "android", "darwin", "ios", "linux", "windows":
537	default:
538		panic("purego: struct arguments/returns in callbacks are only supported on android, darwin, ios, linux, and windows")
539	}
540}
541
542// isDarwin is true on platforms that use Apple's calling convention.
543// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
544const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
545
546func roundUpTo8(val uintptr) uintptr {
547	return (val + align8ByteMask) &^ align8ByteMask
548}
549
550func numOfFloatRegisters() int {
551	switch runtime.GOARCH {
552	case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
553		return 8
554	case "s390x":
555		return 4
556	case "arm":
557		return 16
558	case "386":
559		// i386 SysV ABI passes all arguments on the stack, including floats
560		return 0
561	default:
562		// since this platform isn't supported and can therefore only access
563		// integer registers it is safest to return 8
564		return 8
565	}
566}
567
568func numOfIntegerRegisters() int {
569	switch runtime.GOARCH {
570	case "arm64", "loong64", "ppc64le", "riscv64":
571		return 8
572	case "amd64":
573		return 6
574	case "s390x":
575		// S390X uses R2-R6 for integer arguments
576		return 5
577	case "arm":
578		return 4
579	case "386":
580		// i386 SysV ABI passes all arguments on the stack
581		return 0
582	default:
583		// since this platform isn't supported and can therefore only access
584		// integer registers it is fine to return the maxArgs
585		return maxArgs
586	}
587}
588
589// estimateStackBytes estimates stack bytes needed for Darwin ARM64 validation.
590// This is a conservative estimate used only for early error detection.
591func estimateStackBytes(ty reflect.Type) int {
592	var numInts, numFloats int
593	var stackBytes int
594
595	for i := 0; i < ty.NumIn(); i++ {
596		arg := ty.In(i)
597		size := int(arg.Size())
598
599		// Check if this goes to register or stack
600		usesInt := arg.Kind() != reflect.Float32 && arg.Kind() != reflect.Float64
601		if usesInt && numInts < numOfIntegerRegisters() {
602			numInts++
603		} else if !usesInt && numFloats < numOfFloatRegisters() {
604			numFloats++
605		} else {
606			// Goes to stack - accumulate total bytes
607			stackBytes += size
608		}
609	}
610	// Round total to 8-byte boundary
611	if stackBytes > 0 && stackBytes%align8ByteSize != 0 {
612		stackBytes = int(roundUpTo8(uintptr(stackBytes)))
613	}
614	return stackBytes
615}

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