From 01626432c42d8c2c28c25a36928262d5da72eb3b Mon Sep 17 00:00:00 2001 From: kamkow1 Date: Wed, 15 Oct 2025 19:42:17 +0200 Subject: [PATCH] MOP2 programming intro - add pipes --- content/blog/MOP2/intro.adoc | 168 ++++++++++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/content/blog/MOP2/intro.adoc b/content/blog/MOP2/intro.adoc index 4085cf0..0990767 100644 --- a/content/blog/MOP2/intro.adoc +++ b/content/blog/MOP2/intro.adoc @@ -223,7 +223,7 @@ quite a bit of "freestanding" libraries that you can find online ;(. Printf rant over... -=== Error codes in MOP2 +==== Error codes in MOP2 - a small anecdote You might've noticed is that `main()` looks a little different from standard C `main()`. There's no return/error code, because MOP2 simply does not implement such feature. This is because MOP2 doesn't follow the @@ -243,3 +243,169 @@ so for eg. in MOP2, we have `$fs` for working with the filesystem or `$pctl` for approach things the MOP2 way, it turns out error codes are kind of useless (or at least they wouldn't get much use), since we don't need to connect many programs together to get something done. +=== Printing under the hood - intro to pipes + +Let's take a look into what calling `uprintf()` actually does to print the characters post formatting. The printf +library requires the user to define a `putchar_()` function, which is used to render a single character. +Personally, I think that this way of printing text is inefficient and it would be better to output and entire +buffer of memory, but oh well. + +.putchar.c +[source,c] +---- +#include +#include +#include + +void putchar_(char c) { + ipc_pipewrite(-1, 0, (uint8_t *const)&c, 1); +} +---- + +To output a single character we write it into a pipe. -1 means that the pipe belongs to the calling process, 0 +is an ID into a table of process' pipes - and 0 means percisely the output pipe. In UNIX, the standard pipes are +numbered as 0 = stdin, 1 = stdout and 2 = stderr. In MOP2 there's no stderr, everything the application outputs +goes into the out pipe (0), so we can just drop that entirely. We're left with stdin/in pipe and stdout/out pipe, +but I've decided to swap them around, because the out pipe is used more frequently and it made sense to get it +working first and only then worry about getting input. + +== Pipes + +MOP2 pipes are a lot like UNIX pipes - they're a bidirectional stream of data, but there's slight difference in +the interface. Let's take a look at what ulib defines: + +.Definitions for ipc_pipeXXX() calls +[source,c] +---- +int32_t ipc_piperead(PID_t pid, uint64_t pipenum, uint8_t *const buffer, size_t len); +int32_t ipc_pipewrite(PID_t pid, uint64_t pipenum, const uint8_t *buffer, size_t len); +int32_t ipc_pipemake(uint64_t pipenum); +int32_t ipc_pipedelete(uint64_t pipenum); +int32_t ipc_pipeconnect(PID_t pid1, uint64_t pipenum1, PID_t pid2, uint64_t pipenum2); +---- + +In UNIX you have 2 processes working with a single pipe, but in MOP2, a pipe is exposed to the outside world and +anyone can read and write to it, which explains why these calls require a PID to be provided (indicates the +owner of the pipe). + +.Example of ipc_piperead() - reading your applications own input stream +[source,c] +---- +#include +#include +#include + +void main(void) { + PID_t pid = proc_getpid(); + + #define INPUT_LINE_MAX 1024 + + for (;;) { + char buffer[INPUT_LINE_MAX]; + string_memset(buffer, 0, sizeof(buffer)); + int32_t nrd = ipc_piperead(pid, 1, (uint8_t *const)buffer, sizeof(buffer) - 1); + if (nrd > 0) { + uprintf("Got something: %s\n", buffer); + } + } +} +---- + +`ipc_pipewrite()` is a little boring, so let's not go over it. Creating, deleting and connecting pipes is where +things get interesting. + +A common issue, I've encountered, while programming in userspace for MOP2 is that I'd want to spawn some external +application and collect it's output, for eg. into an ulib `StringBuffer` or some other akin structure. The +obvious thing to do would be to (since everything is polling-based) spawn an application, poll it's state (not +PROC_DEAD) and while polling, read it's out pipe (0) and save it into a stringbuffer. The code to do this would +look something like this: + +.Pipe lifetime problem illustration +[source,c] +---- +#include +#include +#include + +void main(void) { + StringBuffer outsbuf; + stringbuffer_init(&outsbuf); + + char *appargs = { "-saystring", "hello world" }; + int32_t myapp = proc_spawn("base:/bin/myapp", appargs, ARRLEN(appargs)); + + proc_run(myapp); + + // 4 == PROC_DEAD + while (proc_pollstate(myapp) != 4) { + int32_t r; + char buf[100]; + string_memset(buf, 0, sizeof(buf)); + + r = ipc_piperead(myapp, 0, (uint8_t *const)buf, sizeof(buf) - 1); + if (r > 0) { + stringbuffer_appendcstr(&outsbuf, buf); + } + } + + // print entire output + uprintf("%.*s\n", (int)outsbuf.count, outsbuf.data); + + stringbuffer_free(&outsbuf); +} +---- + +Can you spot the BIG BUG? What if the application dies before we manage to read data from the pipe, taking the pipe +down with itself? We're then stuck in this weird state of having incomplete data and the app being reported as +dead by proc_pollstate. + +This can be easily solved by changing the lifetime of the pipe we're working with. The *parent* process shall +allocate a pipe, connect it to it's *child* process and make it so that a child is writing into a pipe managed by +it's parent. + +.Pipe lifetime problem - the solution +[source,c] +---- +#include +#include +#include + +void main(void) { + PID_t pid = proc_getpid(); + + StringBuffer outsbuf; + stringbuffer_init(&outsbuf); + + char *appargs = { "-saystring", "hello world" }; + int32_t myapp = proc_spawn("base:/bin/myapp", appargs, ARRLEN(appargs)); + + // take a free pipe slot. 0 and 1 are already taken by default + ipc_pipemake(10); + // connect pipes + // myapp's out (0) pipe --> pid's 10th pipe + ipc_pipeconnect(myapp, 0, pid, 10); + + proc_run(myapp); + + // 4 == PROC_DEAD + while (proc_pollstate(myapp) != 4) { + int32_t r; + char buf[100]; + string_memset(buf, 0, sizeof(buf)); + + r = ipc_piperead(myapp, 0, (uint8_t *const)buf, sizeof(buf) - 1); + if (r > 0) { + stringbuffer_appendcstr(&outsbuf, buf); + } + } + + // print entire output + uprintf("%.*s\n", (int)outsbuf.count, outsbuf.data); + + ipc_pipedelete(10); + + stringbuffer_free(&outsbuf); +} +---- + +Now, since the parent is managing the pipe and it outlives the child, everything is safe.