Skip to content

"Hello, World!" in PipelineC. A Big Lie

Julian Kemmerer edited this page Nov 29, 2020 · 20 revisions

For anyone just browsing around and thinking 'Hello World' must be the go-to first example for PipelineC - it is not - see digital logic basics instead as a start.

Hi Folks,

I want to share with you a "programming language" based on C called PipelineC. I wrote some code in PipelineC that mimics POSIX open,write,etc standard library functionality. So we can use that to write "Hello, World!" now. As the title says - lies are afoot so don't take terms too seriously. It may look like regular C code, but is actually not meant to 'run' on a CPU or GPU. Read on if you're curious and want to find out whats going on.

Here is a minimal "Hello, World!" in standard C.

    int main() 
    {      
        int fd = open("/dev/stdout", O_RDWR);
        write(fd, "Hello, World!\n", 14);
    } 

So how does "Hello, World!" work in PipelineC?

In PipelineC there are no actual standard C libraries because the "execution model" is not the same. In PipelineC the main() function(s) are "executing" or "being run" over and over again, as if in an outer infinite loop. Each "iteration" of a main() function must return (cannot exit, loop forever, make any arbitrary function calls that might not return, etc).

This might sound a little weird. But if you have ever coded up a control loop - perhaps for something like a microcontroller - you might be familiar with the idea of writing code inside a callback/interrupt routine/function that runs over and over again. Most of the time these routines are simple, just moving around bytes, think basic programming 101 C code.

Your "looping" main() must first open the file. System calls are broken into two parts: request and response. For instance, request to open path "/dev/stdout" and receive response with the file descriptor of the opened file.

Each "iteration" of the main function could output/return an arbitrary number of simultaneous system call request structs. Similarly each "iteration" could receive/input an arbitrary number of simultaneous system call response structs. However, in the below example we keep it simple, one system call "in flight" at a time. Make a request, wait for the response, move on. Inputs and return of the main function are how you receive and send responses and requests during each iteration.

Here is the full code for "Hello, World!". Below are the most important lines with a bit of pseudo code condensing done for clarity.

The state machine in this example does as follows to accomplish Hello World:

  1. Requests to open "/dev/stdout"
  2. Waits for the response from the "operating system" with the opened file descriptor
  3. Requests to write "Hello, World!" to opened file descriptor
  4. Waits for the response from the "operating system" with number of bytes written

PipelineC Hello World

    // Includes omitted... 
    // Input and return type defs omitted...
    
    // Each "iteration" of main() updates a state machine
    // State machine to do the steps of
    typedef enum state_t {
    	OPEN_REQ, // Ask to open the file (starts here)
    	OPEN_RESP, // Receive file descriptor
    	WRITE_REQ, // Ask to write to file descriptor
    	WRITE_RESP, // Receive how many bytes were written
        DONE // Final state
    } state_t;
    state_t state;
    fd_t fildes; // File descriptor for stdout

    outputs_t main(inputs_t input)
    {
      outputs_t output; // Default all zeros
      
      // State machine
      // What gets done this iteration?
      if(state==OPEN_REQ)
      {
        // Make valid request to open /dev/stdout
        output.sys_open.req.path = "/dev/stdout";
        output.sys_open.req.valid = 1;
        // But the receiver of the request might not have been ready
        // So keep outputting request until finally was ready
        if(input.sys_open.req_ready)
        {
          // Then wait for response to request
          state = OPEN_RESP;
        }
      }
      else if(state==OPEN_RESP)
      {
        // Wait in this state ready for response
        output.sys_open.resp_ready = 1;
        // Until we get valid response
        if(input.sys_open.resp.valid)
        { 
          // Save file descriptor from response
          fildes = input.sys_open.resp.fildes;
          // Move onto writing to file by making write request
          state = WRITE_REQ;
        }
      }
      else if(state==WRITE_REQ)
      {
        // Make valid request to write "Hello, World!\n" to the file descriptor
        output.sys_write.req.buf = "Hello, World!\n";
        output.sys_write.req.nbyte = 14;
        output.sys_write.req.fildes = fildes;
        output.sys_write.req.valid = 1;
        // But the receiver of the request might not have been ready
        // So keep outputting request until finally was ready
        if(input.sys_write.req_ready)
        {
          // Then wait for response to request
          state = WRITE_RESP;
        }
      }
      else if(state==WRITE_RESP)
      {
        // Wait in this state ready for response
        output.sys_write.resp_ready = 1;
        // Until we get valid response
        if(input.sys_write.resp.valid)
        { 
          // Would do something based on how many bytes were written
          //    input.sys_write.resp.nbyte
          // But for now assume it went well, done.
          state = DONE;
        }
      }
      
      return output;
    }

Its not simple. But I hope it's at least understandable for programmers familiar with C and POSIX libraries.

Whats the lie?

The operating system handling these "system calls" is actually the OS on an Amazon EC2 F1 FPGA instance. The FPGA attached to that instance is running the state machine described by the above PipelineC code (synthesizeable VHDL).

Tricked into being interested in FPGAs? Check out this example that reads from a file on the host disk, writes the data into an FPGA "block RAM file", and then writes that file back to the host disk. Want more info on the request and responses of this POSIX experiment in PipelineC? Maybe see the PipelineC GitHub for general info on the language.

I am looking for help on a few fronts:

  1. Improving this POSIX thing. Wouldn't it be great to have widely usable "operating system" (hah) for boiler plate FPGA code? PipelineC allows for "cross platform" code. I could get this running on a dev board I have at home too. Anyone with operating systems expertise would be great.
  2. Developing the PipelineC compiler, language, and debug tools. Right now I am trying to keep gcc compatibility to have something to test with. But if this was a real software language, with real software side support we could really go wild with hardware specific features and debug hooks included.
  3. General ideas on what to do next. Got a cool demo project idea? Lets do something cool together!

Very happy to answer questions. Thanks for your time folks.

julian.v.kemmerer@gmail.com