mirror of
https://github.com/taigrr/gopher-os
synced 2025-01-18 04:43:13 -08:00
Support pluggable output sinks for Printf with a ring-buffer fallback
The implementation of Printf has been moved from the early package to the kfmt package. The dependency to ActiveTerminal has been removed and the code now uses an io.Writer for its output. As Go interfaces cannot be used before bootstrapping the Go runtime, the code uses a ring-buffer fallback for storing any kernel output emitted before that point.
This commit is contained in:
parent
f691d75b29
commit
545a18fccc
1
Makefile
1
Makefile
@ -170,6 +170,7 @@ lint: lint-check-deps
|
||||
--deadline 300s \
|
||||
--exclude 'return value not checked' \
|
||||
--exclude 'possible misuse of unsafe.Pointer' \
|
||||
--exclude 'x \^ 0 always equals x' \
|
||||
./...
|
||||
|
||||
lint-check-deps:
|
||||
|
@ -1,20 +1,48 @@
|
||||
package early
|
||||
package kfmt
|
||||
|
||||
import "gopheros/kernel/hal"
|
||||
import (
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// maxBufSize defines the buffer size for formatting numbers.
|
||||
const maxBufSize = 32
|
||||
|
||||
var (
|
||||
errMissingArg = []byte("(MISSING)")
|
||||
errWrongArgType = []byte("%!(WRONGTYPE)")
|
||||
errNoVerb = []byte("%!(NOVERB)")
|
||||
errExtraArg = []byte("%!(EXTRA)")
|
||||
padding = byte(' ')
|
||||
trueValue = []byte("true")
|
||||
falseValue = []byte("false")
|
||||
|
||||
numFmtBuf = []byte("012345678901234567890123456789012")
|
||||
|
||||
// singleByte is used as a shared buffer for passing single characters
|
||||
// to doWrite.
|
||||
singleByte = []byte(" ")
|
||||
|
||||
// earlyPrintBuffer is a ring buffer that stores Printf output before the
|
||||
// console and TTYs are initialized.
|
||||
earlyPrintBuffer ringBuffer
|
||||
|
||||
// outputSink is a io.Writer where Printf will send its output. If set
|
||||
// to nil, then the output will be redirected to the earlyPrintBuffer.
|
||||
outputSink io.Writer
|
||||
)
|
||||
|
||||
// Printf provides a minimal Printf implementation that can be used before the
|
||||
// Go runtime has been properly initialized. This version of printf does not
|
||||
// allocate any memory and uses hal.ActiveTerminal for its output.
|
||||
// SetOutputSink sets the default target for calls to Printf to w and copies
|
||||
// any data accumulated in the earlyPrintBuffer to itt .
|
||||
func SetOutputSink(w io.Writer) {
|
||||
outputSink = w
|
||||
if w != nil {
|
||||
io.Copy(w, &earlyPrintBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
// Printf provides a minimal Printf implementation that can be safely used
|
||||
// before the Go runtime has been properly initialized. This implementation
|
||||
// does not allocate any memory.
|
||||
//
|
||||
// Similar to fmt.Printf, this version of printf supports the following subset
|
||||
// of formatting verbs:
|
||||
@ -46,7 +74,17 @@ var (
|
||||
// starts generating calls to runtime.convT2E (which calls runtime.newobject)
|
||||
// when assembling the argument slice which obviously will crash the kernel since
|
||||
// memory management is not yet available.
|
||||
//
|
||||
// The output of Printf is written to the currently active TTY. If no TTY is
|
||||
// available, then the output is buffered into a ring-buffer and can be
|
||||
// retrieved by a call to FlushRingBuffer.
|
||||
func Printf(format string, args ...interface{}) {
|
||||
Fprintf(outputSink, format, args...)
|
||||
}
|
||||
|
||||
// Fprintf behaves exactly like Printf but it writes the formatted output to
|
||||
// the specified io.Writer.
|
||||
func Fprintf(w io.Writer, format string, args ...interface{}) {
|
||||
var (
|
||||
nextCh byte
|
||||
nextArgIndex int
|
||||
@ -62,8 +100,11 @@ func Printf(format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
if blockStart < blockEnd {
|
||||
// passing format[blockStart:blockEnd] to doWrite triggers a
|
||||
// memory allocation so we need to do this one byte at a time.
|
||||
for i := blockStart; i < blockEnd; i++ {
|
||||
hal.ActiveTerminal.WriteByte(format[i])
|
||||
singleByte[0] = format[i]
|
||||
doWrite(w, singleByte)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +116,8 @@ func Printf(format string, args ...interface{}) {
|
||||
nextCh = format[blockEnd]
|
||||
switch {
|
||||
case nextCh == '%':
|
||||
hal.ActiveTerminal.Write([]byte{'%'})
|
||||
singleByte[0] = '%'
|
||||
doWrite(w, singleByte)
|
||||
break parseFmt
|
||||
case nextCh >= '0' && nextCh <= '9':
|
||||
padLen = (padLen * 10) + int(nextCh-'0')
|
||||
@ -83,21 +125,21 @@ func Printf(format string, args ...interface{}) {
|
||||
case nextCh == 'd' || nextCh == 'x' || nextCh == 'o' || nextCh == 's' || nextCh == 't':
|
||||
// Run out of args to print
|
||||
if nextArgIndex >= len(args) {
|
||||
hal.ActiveTerminal.Write(errMissingArg)
|
||||
doWrite(w, errMissingArg)
|
||||
break parseFmt
|
||||
}
|
||||
|
||||
switch nextCh {
|
||||
case 'o':
|
||||
fmtInt(args[nextArgIndex], 8, padLen)
|
||||
fmtInt(w, args[nextArgIndex], 8, padLen)
|
||||
case 'd':
|
||||
fmtInt(args[nextArgIndex], 10, padLen)
|
||||
fmtInt(w, args[nextArgIndex], 10, padLen)
|
||||
case 'x':
|
||||
fmtInt(args[nextArgIndex], 16, padLen)
|
||||
fmtInt(w, args[nextArgIndex], 16, padLen)
|
||||
case 's':
|
||||
fmtString(args[nextArgIndex], padLen)
|
||||
fmtString(w, args[nextArgIndex], padLen)
|
||||
case 't':
|
||||
fmtBool(args[nextArgIndex])
|
||||
fmtBool(w, args[nextArgIndex])
|
||||
}
|
||||
|
||||
nextArgIndex++
|
||||
@ -105,80 +147,87 @@ func Printf(format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
// reached end of formatting string without finding a verb
|
||||
hal.ActiveTerminal.Write(errNoVerb)
|
||||
doWrite(w, errNoVerb)
|
||||
}
|
||||
blockStart, blockEnd = blockEnd+1, blockEnd+1
|
||||
}
|
||||
|
||||
if blockStart != blockEnd {
|
||||
// passing format[blockStart:blockEnd] to doWrite triggers a
|
||||
// memory allocation so we need to do this one byte at a time.
|
||||
for i := blockStart; i < blockEnd; i++ {
|
||||
hal.ActiveTerminal.WriteByte(format[i])
|
||||
singleByte[0] = format[i]
|
||||
doWrite(w, singleByte)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unused args
|
||||
for ; nextArgIndex < len(args); nextArgIndex++ {
|
||||
hal.ActiveTerminal.Write(errExtraArg)
|
||||
doWrite(w, errExtraArg)
|
||||
}
|
||||
}
|
||||
|
||||
// fmtBool prints a formatted version of boolean value v using hal.ActiveTerminal
|
||||
// for its output.
|
||||
func fmtBool(v interface{}) {
|
||||
// fmtBool prints a formatted version of boolean value v.
|
||||
func fmtBool(w io.Writer, v interface{}) {
|
||||
switch bVal := v.(type) {
|
||||
case bool:
|
||||
switch bVal {
|
||||
case true:
|
||||
hal.ActiveTerminal.Write(trueValue)
|
||||
doWrite(w, trueValue)
|
||||
case false:
|
||||
hal.ActiveTerminal.Write(falseValue)
|
||||
doWrite(w, falseValue)
|
||||
}
|
||||
default:
|
||||
hal.ActiveTerminal.Write(errWrongArgType)
|
||||
doWrite(w, errWrongArgType)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// fmtString prints a formatted version of string or []byte value v, applying the
|
||||
// padding specified by padLen. This function uses hal.ActiveTerminal for its
|
||||
// output.
|
||||
func fmtString(v interface{}, padLen int) {
|
||||
// fmtString prints a formatted version of string or []byte value v, applying
|
||||
// the padding specified by padLen.
|
||||
func fmtString(w io.Writer, v interface{}, padLen int) {
|
||||
switch castedVal := v.(type) {
|
||||
case string:
|
||||
fmtRepeat(padding, padLen-len(castedVal))
|
||||
fmtRepeat(w, ' ', padLen-len(castedVal))
|
||||
// converting the string to a byte slice triggers a memory allocation
|
||||
// so we need to do this one byte at a time.
|
||||
for i := 0; i < len(castedVal); i++ {
|
||||
hal.ActiveTerminal.WriteByte(castedVal[i])
|
||||
singleByte[0] = castedVal[i]
|
||||
doWrite(w, singleByte)
|
||||
}
|
||||
case []byte:
|
||||
fmtRepeat(padding, padLen-len(castedVal))
|
||||
hal.ActiveTerminal.Write(castedVal)
|
||||
fmtRepeat(w, ' ', padLen-len(castedVal))
|
||||
doWrite(w, castedVal)
|
||||
default:
|
||||
hal.ActiveTerminal.Write(errWrongArgType)
|
||||
doWrite(w, errWrongArgType)
|
||||
}
|
||||
}
|
||||
|
||||
// fmtRepeat writes count bytes with value ch to the hal.ActiveTerminal.
|
||||
func fmtRepeat(ch byte, count int) {
|
||||
// fmtRepeat writes count bytes with value ch.
|
||||
func fmtRepeat(w io.Writer, ch byte, count int) {
|
||||
singleByte[0] = ch
|
||||
for i := 0; i < count; i++ {
|
||||
hal.ActiveTerminal.WriteByte(ch)
|
||||
doWrite(w, singleByte)
|
||||
}
|
||||
}
|
||||
|
||||
// fmtInt prints out a formatted version of v in the requested base, applying the
|
||||
// padding specified by padLen. This function uses hal.ActiveTerminal for its
|
||||
// output, supports all built-in signed and unsigned integer types and supports
|
||||
// base 8, 10 and 16 output.
|
||||
func fmtInt(v interface{}, base, padLen int) {
|
||||
// fmtInt prints out a formatted version of v in the requested base, applying
|
||||
// the padding specified by padLen. This function supports all built-in signed
|
||||
// and unsigned integer types and base 8, 10 and 16 output.
|
||||
func fmtInt(w io.Writer, v interface{}, base, padLen int) {
|
||||
var (
|
||||
sval int64
|
||||
uval uint64
|
||||
divider uint64
|
||||
remainder uint64
|
||||
buf [20]byte
|
||||
padCh byte
|
||||
left, right, end int
|
||||
)
|
||||
|
||||
if padLen >= maxBufSize {
|
||||
padLen = maxBufSize - 1
|
||||
}
|
||||
|
||||
switch base {
|
||||
case 8:
|
||||
divider = 8
|
||||
@ -213,7 +262,7 @@ func fmtInt(v interface{}, base, padLen int) {
|
||||
case int:
|
||||
sval = int64(v.(int))
|
||||
default:
|
||||
hal.ActiveTerminal.Write(errWrongArgType)
|
||||
doWrite(w, errWrongArgType)
|
||||
return
|
||||
}
|
||||
|
||||
@ -224,13 +273,13 @@ func fmtInt(v interface{}, base, padLen int) {
|
||||
uval = uint64(sval)
|
||||
}
|
||||
|
||||
for {
|
||||
for right < maxBufSize {
|
||||
remainder = uval % divider
|
||||
if remainder < 10 {
|
||||
buf[right] = byte(remainder) + '0'
|
||||
numFmtBuf[right] = byte(remainder) + '0'
|
||||
} else {
|
||||
// map values from 10 to 15 -> a-f
|
||||
buf[right] = byte(remainder-10) + 'a'
|
||||
numFmtBuf[right] = byte(remainder-10) + 'a'
|
||||
}
|
||||
|
||||
right++
|
||||
@ -243,27 +292,55 @@ func fmtInt(v interface{}, base, padLen int) {
|
||||
|
||||
// Apply padding if required
|
||||
for ; right-left < padLen; right++ {
|
||||
buf[right] = padCh
|
||||
numFmtBuf[right] = padCh
|
||||
}
|
||||
|
||||
// Apply negative sign to the rightmost blank character (if using enough padding);
|
||||
// otherwise append the sign as a new char
|
||||
if sval < 0 {
|
||||
for end = right - 1; buf[end] == ' '; end-- {
|
||||
for end = right - 1; numFmtBuf[end] == ' '; end-- {
|
||||
}
|
||||
|
||||
if end == right-1 {
|
||||
right++
|
||||
}
|
||||
|
||||
buf[end+1] = '-'
|
||||
numFmtBuf[end+1] = '-'
|
||||
}
|
||||
|
||||
// Reverse in place
|
||||
end = right
|
||||
for right = right - 1; left < right; left, right = left+1, right-1 {
|
||||
buf[left], buf[right] = buf[right], buf[left]
|
||||
numFmtBuf[left], numFmtBuf[right] = numFmtBuf[right], numFmtBuf[left]
|
||||
}
|
||||
|
||||
hal.ActiveTerminal.Write(buf[0:end])
|
||||
doWrite(w, numFmtBuf[0:end])
|
||||
}
|
||||
|
||||
// doWrite is a proxy that uses the runtime.noescape hack to hide p from the
|
||||
// compiler's escape analysis. Without this hack, the compiler cannot properly
|
||||
// detect that p does not escape (due to the call to the yet unknown outputSink
|
||||
// io.Writer) and plays it safe by flagging it as escaping. This causes all
|
||||
// calls to Printf to call runtime.convT2E which triggers a memory allocation
|
||||
// causing the kernel to crash if a call to Printf is made before the Go
|
||||
// allocator is initialized.
|
||||
func doWrite(w io.Writer, p []byte) {
|
||||
doRealWrite(w, noEscape(unsafe.Pointer(&p)))
|
||||
}
|
||||
|
||||
func doRealWrite(w io.Writer, bufPtr unsafe.Pointer) {
|
||||
p := *(*[]byte)(bufPtr)
|
||||
if w != nil {
|
||||
w.Write(p)
|
||||
} else {
|
||||
earlyPrintBuffer.Write(p)
|
||||
}
|
||||
}
|
||||
|
||||
// noEscape hides a pointer from escape analysis. This function is copied over
|
||||
// from runtime/stubs.go
|
||||
//go:nosplit
|
||||
func noEscape(p unsafe.Pointer) unsafe.Pointer {
|
||||
x := uintptr(p)
|
||||
return unsafe.Pointer(x ^ 0)
|
||||
}
|
@ -1,31 +1,20 @@
|
||||
package early
|
||||
package kfmt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"gopheros/kernel/driver/tty"
|
||||
"gopheros/kernel/driver/video/console"
|
||||
"gopheros/kernel/hal"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TestPrintf(t *testing.T) {
|
||||
origTerm := hal.ActiveTerminal
|
||||
defer func() {
|
||||
hal.ActiveTerminal = origTerm
|
||||
outputSink = nil
|
||||
}()
|
||||
|
||||
// mute vet warnings about malformed printf formatting strings
|
||||
printfn := Printf
|
||||
|
||||
ega := &console.Ega{}
|
||||
fb := make([]uint8, 160*25)
|
||||
ega.Init(80, 25, uintptr(unsafe.Pointer(&fb[0])))
|
||||
|
||||
vt := &tty.Vt{}
|
||||
vt.AttachTo(ega)
|
||||
hal.ActiveTerminal = vt
|
||||
|
||||
specs := []struct {
|
||||
fn func()
|
||||
expOutput string
|
||||
@ -124,6 +113,10 @@ func TestPrintf(t *testing.T) {
|
||||
func() { printfn("int arg longer than padding: '%5x'", int(-0xbadf00d)) },
|
||||
"int arg longer than padding: '-badf00d'",
|
||||
},
|
||||
{
|
||||
func() { printfn("padding longer than maxBufSize '%128x'", int(-0xbadf00d)) },
|
||||
fmt.Sprintf("padding longer than maxBufSize '-%sbadf00d'", strings.Repeat("0", maxBufSize-8)),
|
||||
},
|
||||
// multiple arguments
|
||||
{
|
||||
func() { printfn("%%%s%d%t", "foo", 123, true) },
|
||||
@ -156,25 +149,42 @@ func TestPrintf(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
for index := 0; index < len(fb); index++ {
|
||||
fb[index] = 0
|
||||
}
|
||||
vt.SetPosition(0, 0)
|
||||
var buf bytes.Buffer
|
||||
SetOutputSink(&buf)
|
||||
|
||||
for specIndex, spec := range specs {
|
||||
buf.Reset()
|
||||
spec.fn()
|
||||
|
||||
var buf bytes.Buffer
|
||||
for index := 0; ; index += 2 {
|
||||
if fb[index] == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
buf.WriteByte(fb[index])
|
||||
}
|
||||
|
||||
if got := buf.String(); got != spec.expOutput {
|
||||
t.Errorf("[spec %d] expected to get %q; got %q", specIndex, spec.expOutput, got)
|
||||
t.Errorf("[spec %d] expected to get\n%q\ngot:\n%q", specIndex, spec.expOutput, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintfToRingBuffer(t *testing.T) {
|
||||
defer func() {
|
||||
outputSink = nil
|
||||
}()
|
||||
|
||||
exp := "hello world"
|
||||
Fprintf(&buf, exp)
|
||||
|
||||
var buf bytes.Buffer
|
||||
SetOutputSink(buf)
|
||||
|
||||
if got := buf.String(); got != exp {
|
||||
t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFprintf(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
exp := "hello world"
|
||||
Fprintf(&buf, exp)
|
||||
|
||||
if got := buf.String(); got != exp {
|
||||
t.Fatalf("expected to get:\n%q\ngot:\n%q", exp, got)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user