MOP2 programming intro - add pipes

This commit is contained in:
2025-10-15 19:42:17 +02:00
parent 7b80fa2842
commit 01626432c4

View File

@ -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 <stdint.h>
#include <system/system.h>
#include <printf/printf.h>
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 <stddef.h>
#include <stdint.h>
#include <ulib.h>
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 <stddef.h>
#include <stdint.h>
#include <ulib.h>
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 <stddef.h>
#include <stdint.h>
#include <ulib.h>
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.