Hello world part II : actually recoding print

In part I we explored the pre-requisite in order to code print : having a grasp on the framebuffer.

Here, we are gonna deep inside one of the most overlooked object oriented abastraction : a file and actually print what we can of hello world in 100 lines of code.


The file handler and the file descriptor



These two abstractions are the low level and high level abstractions of the same thing : a view on something more complex which access has been encapsulated in generic methods. Actually when you code a framebuffer driver you provide function pointers that are specialized to your device and you may omit those common to the class. This is done with a double lookup on the major node, minor node number. Of those « generic » methods you have : seek, write, tell, truncate, open, read, close ...
The file handler in python also handles extra bytes (pun) of facilities : like character encoding, stats, and buffering.

Here, we work with the low level abstraction : the file which we access with fileno through its file descriptor. And thanks to this abstraction, you don't care if the underlying implementation fragments the file itself (ex: on a hard drive), you can magically always ask without caring for the gory details to read any arbitrary block or chararacters at any given position.

Two of the most used methods on files here are seek and write.

The file descriptor method write is sensitive to the positionning set by seek. Hence, we can write a line, and position ourselves one line below to write the next line in a character.

matrices as a view of a row based array



When I speak of rows and columns I evocate abstractions that are not related to the framebuffer.

The coordinates are an abstraction we build for convenience to say I want to write from this line at this column.
And since human beings bug after 2 dimensions we split the last dimnension in a vector of dimension 4 called a pixel.
get_at function illustrates our use of this trick to position the (invisible) cursor at any given position on the screen expressed for clarity in size of glyphes.
We could actually code all this exercice through a 3D view of the framebuffer. I just wouldn't be able to pack the code in less than 100 lines of code and would introduce useless abstractions.

But if you have doubt on the numerous seek I do and why I mutiply lines and columns value the way I do check the preceding link for an understanding of raw based array nth matricial dimensionnal views.

fonts, chars glyphs...



Here we are gonna take matrices defining the glyphes (what you actually see on screen) by 8x8 = 64 1D array and map them on the screen with put_char. Put char does a little bit of magic by relying on python to do the chararcter to glyph conversion through the dict lookup that expecting strings does a de factor codepoint to glyph conversion without having to pass the codepoint conversion.

The set of characters to glyphs conversion with their common property is a font.

The hidden console



The console is an abstraction that keeps track of the global states such as : what is the current line we print at. Thus, here, being lazy I use the global variables instead of a singleton named « console » or « term » to keep track of them. But first and foremost, these « abstractions » are just expectations we share in a common mental mode. Like we expect « print » to add a newline at the end of the string and begin the printing at the next line.

The to be finished example



I limited the code to 100 lines so that it's fairly readable. I let as an exercise the following points :
  • encoding the missing glyphes in the font to actually be able to write "hello world!",
  • handling the edge case of reaching the bottom of the screen,
I want to point out the true python print function is WAY MORE COMPLEX than this terse example and also handle magic conversion from memory objects to their string counterpart (like integers that are converted to their decimal representation), it handles buffering, encoding, and so much more. This is merely a toy to dive into the complexity of the mission at hand.
This example is a part of a project to write « hello world » on the framebuffer in numerous languages, bash included.

Annexe : the code




#!/usr/bin/env python3
from struct import pack
from os import SEEK_CUR, lseek as  seek, write
w,h =map(int, open("/sys/class/graphics/fb0/virtual_size").read().split(","))
so_pixel = 4
stride = w * so_pixel

encode = lambda b,g,r,a : pack("4B",b,g,r,a)

font = {
    "height":8,
    "width":8,
    'void' : [ 
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 1, 1, 1, 1, 0, 0,
        0, 1, 0, 0, 0, 0, 1, 0, 
        0, 0, 1, 0, 0, 1, 1, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0, 
       ],
    "l":[ 
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 0, 1, 1, 1, 1, 0, 0,
        ],
    "o": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 1, 1, 1, 1, 0, 0,
        0, 1, 0, 0, 0, 0, 1, 0,
        0, 1, 0, 0, 0, 0, 1, 0,
        0, 1, 0, 0, 0, 0, 1, 0,
        0, 1, 0, 0, 0, 0, 1, 0,
        0, 0, 1, 0, 0, 1, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0],
    "h": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 0, 0, 0, 0, 0, 0,
        0, 1, 1, 1, 1, 0, 0, 0,
        0, 1, 0, 0, 1, 0, 0, 0,
        0, 1, 1, 0, 1, 0, 0, 0,
        0, 1, 0, 0, 1, 0, 0, 0],
}

def go_at(fh, x, y): 
   global stride
   seek(fh.fileno(),x*so_pixel + y *stride, 0)

def next_line(fh, reminder):
    seek(fh.fileno(), stride - reminder, SEEK_CUR)

def put_char(fh, x,y, letter):
    go_at(fh, x, y)
    black = encode(0,0,0,255)
    white = encode(255,255,255,255)
    char = font.get(letter, None) or font["void"]
    line = ""
    for col,pixel in enumerate(char):
        write(fh.fileno(), white if pixel else black)
        if (col%font["width"]==font["width"]-1):
            next_line(fh, so_pixel * font["width"])
COL=0
LIN=0

OUT = open("/dev/fb0", "bw")
FD = OUT.fileno()

def newline():
    global OUT,LIN,COL
    LIN+=1
    go_at(OUT, 0, LIN * font["height"])

def print_(line):
    global OUT, COL, LIN
    COL=0
    for c in line:
        if c == "\n":
            newline()
        put_char(OUT,COL * font["width"] , LIN * font['height'], c)
        COL+=1
    newline() 

for i in range(30):
    print_("hello lol")

No comments: