v0.11.0-alpha 7618415

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

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