1
0
mirror of https://github.com/taigrr/gopher-os synced 2025-01-18 04:43:13 -08:00

Implement early printf functionality

The kfmt/early package provides a minimal printf implementation that
does not use memory allocations (everything is allocated on the stack).
This implementation can be used to emit debug messages before the memory
manager is initialized.
This commit is contained in:
Achilleas Anagnostopoulos 2017-04-05 08:35:32 +01:00
parent d69c945019
commit b6ad5c933d
2 changed files with 451 additions and 0 deletions

View File

@ -0,0 +1,270 @@
package early
import "github.com/achilleasa/gopher-os/kernel/hal"
var (
errMissingArg = []byte("(MISSING)")
errWrongArgType = []byte("%!(WRONGTYPE)")
errNoVerb = []byte("%!(NOVERB)")
errExtraArg = []byte("%!(EXTRA)")
padding = []byte{' '}
trueValue = []byte("true")
falseValue = []byte("false")
)
// 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.
//
// Similar to fmt.Printf, this version of printf supports the following subset
// of formatting verbs:
//
// Strings:
// %s the uninterpreted bytes of the string or byte slice
//
// Integers:
// %o base 8
// %d base 10
// %x base 16, with lower-case letters for a-f
//
// Booleans:
// %t "true" or "false"
//
// Width is specified by an optional decimal number immediately preceding the verb.
// If absent, the width is whatever is necessary to represent the value.
//
// String values with length less than the specified width will be left-padded with
// spaces. Integer values formatted as base-10 will also be left-padded with spaces.
// Finally, integer values formatted as base-16 will be left-padded with zeroes.
//
// Printf supports all built-in string and integer types but assumes that the
// Go itables have not been initialized yet so it will not check whether its
// arguments support io.Stringer if they don't match one of the supported tupes.
//
// This function does not provide support for printing pointers (%p) as this
// requires importing the reflect package. By importing reflect, the go compiler
// 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.
func Printf(format string, args ...interface{}) {
var (
nextCh byte
nextArgIndex int
blockStart, blockEnd, padLen int
fmtLen = len(format)
)
for blockEnd < fmtLen {
nextCh = format[blockEnd]
if nextCh != '%' {
blockEnd++
continue
}
if blockStart < blockEnd {
hal.ActiveTerminal.Write([]byte(format[blockStart:blockEnd]))
}
// Scan til we hit the format character
padLen = 0
blockEnd++
parseFmt:
for ; blockEnd < fmtLen; blockEnd++ {
nextCh = format[blockEnd]
switch {
case nextCh == '%':
hal.ActiveTerminal.Write([]byte{'%'})
break parseFmt
case nextCh >= '0' && nextCh <= '9':
padLen = (padLen * 10) + int(nextCh-'0')
continue
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)
break parseFmt
}
switch nextCh {
case 'o':
fmtInt(args[nextArgIndex], 8, padLen)
case 'd':
fmtInt(args[nextArgIndex], 10, padLen)
case 'x':
fmtInt(args[nextArgIndex], 16, padLen)
case 's':
fmtString(args[nextArgIndex], padLen)
case 't':
fmtBool(args[nextArgIndex])
}
nextArgIndex++
break parseFmt
}
// reached end of formatting string without finding a verb
hal.ActiveTerminal.Write(errNoVerb)
}
blockStart, blockEnd = blockEnd+1, blockEnd+1
}
if blockStart != blockEnd {
hal.ActiveTerminal.Write([]byte(format[blockStart:blockEnd]))
}
// Check for unused args
for ; nextArgIndex < len(args); nextArgIndex++ {
hal.ActiveTerminal.Write(errExtraArg)
}
}
// fmtBool prints a formatted version of boolean value v using hal.ActiveTerminal
// for its output.
func fmtBool(v interface{}) {
switch bVal := v.(type) {
case bool:
switch bVal {
case true:
hal.ActiveTerminal.Write(trueValue)
case false:
hal.ActiveTerminal.Write(falseValue)
}
default:
hal.ActiveTerminal.Write(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) {
var sval []byte
switch castedVal := v.(type) {
case string:
sval = []byte(castedVal)
case []byte:
sval = castedVal
default:
hal.ActiveTerminal.Write(errWrongArgType)
return
}
for pad := padLen - len(sval); pad > 0; pad-- {
hal.ActiveTerminal.Write(padding)
}
hal.ActiveTerminal.Write(sval)
}
// 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) {
var (
sval int64
uval uint64
divider uint64
remainder uint64
buf [20]byte
padCh byte
left, right, end int
)
switch base {
case 8:
divider = 8
padCh = '0'
case 10:
divider = 10
padCh = ' '
case 16:
divider = 16
padCh = '0'
}
switch v.(type) {
case uint8:
uval = uint64(v.(uint8))
case uint16:
uval = uint64(v.(uint16))
case uint32:
uval = uint64(v.(uint32))
case uint64:
uval = v.(uint64)
case uintptr:
uval = uint64(v.(uintptr))
case int8:
sval = int64(v.(int8))
case int16:
sval = int64(v.(int16))
case int32:
sval = int64(v.(int32))
case int64:
sval = v.(int64)
case int:
sval = int64(v.(int))
default:
hal.ActiveTerminal.Write(errWrongArgType)
return
}
// Handle signs
if sval < 0 {
uval = uint64(-sval)
} else if sval > 0 {
uval = uint64(sval)
}
for {
remainder = uval % divider
if remainder < 10 {
buf[right] = byte(remainder) + '0'
} else {
// map values from 10 to 15 -> a-f
buf[right] = byte(remainder-10) + 'a'
}
right++
uval /= divider
if uval == 0 {
break
}
}
// Apply padding if required
for ; right-left < padLen; right++ {
buf[right] = padCh
}
// Apply hex prefix
if base == 16 {
buf[right] = 'x'
buf[right+1] = '0'
right += 2
}
// 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-- {
}
if end == right-1 {
right++
}
buf[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]
}
hal.ActiveTerminal.Write(buf[0:end])
}

View File

@ -0,0 +1,181 @@
package early
import (
"bytes"
"testing"
"unsafe"
"github.com/achilleasa/gopher-os/kernel/driver/tty"
"github.com/achilleasa/gopher-os/kernel/driver/video/console"
"github.com/achilleasa/gopher-os/kernel/hal"
)
func TestPrintf(t *testing.T) {
origTerm := hal.ActiveTerminal
defer func() {
hal.ActiveTerminal = origTerm
}()
// 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
}{
{
func() { printfn("no args") },
"no args",
},
// bool values
{
func() { printfn("%t", true) },
"true",
},
{
func() { printfn("%41t", false) },
"false",
},
// strings and byte slices
{
func() { printfn("%s arg", "STRING") },
"STRING arg",
},
{
func() { printfn("%s arg", []byte("BYTE SLICE")) },
"BYTE SLICE arg",
},
{
func() { printfn("'%4s' arg with padding", "ABC") },
"' ABC' arg with padding",
},
{
func() { printfn("'%4s' arg longer than padding", "ABCDE") },
"'ABCDE' arg longer than padding",
},
// uints
{
func() { printfn("uint arg: %d", uint8(10)) },
"uint arg: 10",
},
{
func() { printfn("uint arg: %o", uint16(0777)) },
"uint arg: 777",
},
{
func() { printfn("uint arg: %x", uint32(0xbadf00d)) },
"uint arg: 0xbadf00d",
},
{
func() { printfn("uint arg with padding: '%10d'", uint64(123)) },
"uint arg with padding: ' 123'",
},
{
func() { printfn("uint arg with padding: '%4o'", uint64(0777)) },
"uint arg with padding: '0777'",
},
{
func() { printfn("uint arg with padding: '%10x'", uint64(0xbadf00d)) },
"uint arg with padding: '0x000badf00d'",
},
{
func() { printfn("uint arg longer than padding: '%5x'", int64(0xbadf00d)) },
"uint arg longer than padding: '0xbadf00d'",
},
// pointers
{
func() { printfn("uintptr %x", uintptr(0xb8000)) },
"uintptr 0xb8000",
},
// ints
{
func() { printfn("int arg: %d", int8(-10)) },
"int arg: -10",
},
{
func() { printfn("int arg: %o", int16(0777)) },
"int arg: 777",
},
{
func() { printfn("int arg: %x", int32(-0xbadf00d)) },
"int arg: -0xbadf00d",
},
{
func() { printfn("int arg with padding: '%10d'", int64(-12345678)) },
"int arg with padding: ' -12345678'",
},
{
func() { printfn("int arg with padding: '%10d'", int64(-123456789)) },
"int arg with padding: '-123456789'",
},
{
func() { printfn("int arg with padding: '%10d'", int64(-1234567890)) },
"int arg with padding: '-1234567890'",
},
{
func() { printfn("int arg longer than padding: '%5x'", int(-0xbadf00d)) },
"int arg longer than padding: '-0xbadf00d'",
},
// multiple arguments
{
func() { printfn("%%%s%d%t", "foo", 123, true) },
`%foo123true`,
},
// errors
{
func() { printfn("more args", "foo", "bar", "baz") },
`more args%!(EXTRA)%!(EXTRA)%!(EXTRA)`,
},
{
func() { printfn("missing args %s") },
`missing args (MISSING)`,
},
{
func() { printfn("bad verb %Q") },
`bad verb %!(NOVERB)`,
},
{
func() { printfn("not bool %t", "foo") },
`not bool %!(WRONGTYPE)`,
},
{
func() { printfn("not int %d", "foo") },
`not int %!(WRONGTYPE)`,
},
{
func() { printfn("not string %s", 123) },
`not string %!(WRONGTYPE)`,
},
}
for specIndex, spec := range specs {
for index := 0; index < len(fb); index++ {
fb[index] = 0
}
vt.SetPosition(0, 0)
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)
}
}
}