MOP2 programming intro - add pipes
This commit is contained in:
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user