1
0
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:
Achilleas Anagnostopoulos 2017-07-01 07:02:16 +01:00
parent f691d75b29
commit 545a18fccc
3 changed files with 168 additions and 80 deletions

View File

@ -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:

View File

@ -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)
}

View File

@ -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)
}
}