Hello,
I'm writing a kernel in Nim for fun and I'd like to be able to print things with echo. I added stubs for flockfile, funlockfile and fflush, as well as a full function for fwrite that adds characters to the framebuffer.
I don't understand the parameters I'm receiving though.
My fwrite prototype is: proc fwrite(data: ptr byte, size: csize_t, nitems: csize_t, stream: ptr FILE): csize_t {.exportc.} = When I call echo with "Hello world!", it tells me that size is 13 and nitems is 2, whereas I expected size to be 1 (byte), and nitems to be 13 characters (including the terminator).
So my first question is why are those parameters like that? And what do they represent?
A second issue I have is that when I ignore all this and write the characters to buffer using the code below, the first two characters are corrupted and the rest prints fine. I don't know if this is related to my misunderstanding of the parameters.
var charArray: ptr UncheckedArray[char] = cast[ptr UncheckedArray[char]](data)
for i in 0..size:
var c: char = charArray[i]
var info: uint16 = cast[uint8](c) or (colour.uint16 shl 8)
video_memory[i] = info
I have to add the disclaimer that I'm not good at C, Nim or OS development so I'm sure it must come from something stupid I'm doing, and it might even be something not related to this piece of code because of some memory shenanigans from the OS. But I figured I'd check the highest level component first and I appreciate your patience in helping me figure this out.
Well, I think that size is the number of bytes. Each character is stored on a char (1 byte) and "Hello world!" is indeed made of 13 characters in C. The size of the cstring is indeed 13 bytes.
Now, nitems is probably the number of arguments with echo counting as one (similar as argc for command line parameters).
I am not sure, any confirmation (/infirmation) of this would be appreciated.
Thank you for all your replies!
I did increase both by one! I have no debugging facilities yet because I couldn't get gdb to run with QEMU so I figured I'd print a random character in a loop size times, then nbitems times and count them. So I did for i in 0..size... when I should have been doing size-1.
The fact that I expected there to be 13 characters made me think the only issue was with nbitems.
As for the 2 corrupted characters, since echoBinSafe makes a second call discard c_fwrite(linefeed.cstring, linefeed.len, 1, cstdout), they came from the second call overwriting the first in the VGA buffer. One linefeed char to overwrite the H, and a second one from reading something I shouldn't been reading because of using size instead of size-1.
This is the fixed code in case it's ever useful to anyone:
kernel.nim
import stdio
type
TMultiboot_header = object
PMultiboot_header = ptr TMultiboot_header
proc kmain(mb_header: PMultiboot_header, magic: int) {.exportc.} =
if magic != 0x2BADB002:
discard
echo "Hello world!"
echo "Other things."
stdio.nim
type VGABuffer* = ptr array[0..2000, uint16]
type
FILE = object
var stdout* {.exportc.} : ptr FILE
const video_memory = cast[VGABuffer](0xB8000)
var line: uint64 = 0
proc flockfile(f: ptr FILE) {.exportc.} = discard
proc funlockfile(f: ptr FILE) {.exportc.} = discard
proc fflush(stream: ptr FILE): cint {.exportc.} = return cast[cint](0)
proc fwrite(data: cstring, size: csize_t, nitems: csize_t, stream: ptr FILE): csize_t {.exportc.} =
let backColour: byte = 0b00000000
let foreColour: byte = 0b00001111
let colour: byte = (backColour shl 4) or foreColour
for i in 0..size-1:
var c: char = data[i]
var info: uint16 = cast[uint8](c) or (colour.uint16 shl 8)
video_memory[i+(line*80)] = info
line += 1
return nitems
Result
Nim has an operator for non-inclusive iteration ..<. That, some proper handling of newlines, and a little refactor and your code can look like this:
type
VGABuffer* = ptr array[0..2000, uint16]
FILE = object
var stdout* {.exportc.} : ptr FILE
const video_memory = cast[VGABuffer](0xB8000)
var line: uint64 = 0
proc flockfile(f: ptr FILE) {.exportc.} = discard
proc funlockfile(f: ptr FILE) {.exportc.} = discard
proc fflush(stream: ptr FILE): cint {.exportc.} = 0.cint
proc fwrite(data: cstring, size: csize_t, nitems: csize_t, stream: ptr FILE): csize_t {.exportc.} =
let
backColour: byte = 0b00000000
foreColour: byte = 0b00001111
colour: byte = (backColour shl 4) or foreColour
for i in 0..<size:
var
c = data[i]
info = c.uint8 or (colour.uint16 shl 8)
if c != '\n':
video_memory[i+(line*80)] = info
else:
line += 1
return nitems
Of course you might want to add some actual handling of nitems as well.
Here's how I implemented fwrite in my kernel:
type
constCstringImpl {.importc: "const char *".} = cstring
constCstring = distinct constCstringImpl
constPointerImpl {.importc: "const void *".} = pointer
constPointer = distinct constPointerImpl
type
CFile {.importc: "FILE", header: "<stdio.h>", incompleteStruct.} = object
CFilePtr* = ptr CFile ## The type representing a file handle.
proc fwrite*(buf {.noalias.}: constPointer, size, n: csize_t, f {.noalias.}: CFilePtr): csize_t
{.exportc.} =
let p = cast[ptr UncheckedArray[char]](buf)
for i in 0..(n*size):
print(&"{p[i]}")
In my case print uses the UEFI Simple Text Output protocol instead of writing directly to the framebuffer, but that's beside the point.