Oct 10, 2025 - Last edited: Oct 12, 2025
In previous installment, we got the “Hello world!” from a Zig extension for Zephyr, based on Zephyr EDK Sample. Time to expand it!
We’ll now try to replicate the features of the extension that lives in kernel space. Looking at its code (at samples/subsys/llext/edk/k-ext1/src/main.c), we see that it creates a thread that waits for events, and when the events are received - the messages from the pub/sub system - it signals the main thread via a semaphore. Let’s start with receiving the events on the main thread first, then we can play with another thread and semaphores.
The bits of code necessary for waiting for events is fairly straightforward in the C extension:
// (...)
struct k_event *tick_evt = k_object_alloc(K_OBJ_EVENT);
k_event_init(tick_evt);
register_subscriber(CHAN_TICK, tick_evt);
while (true) {
printk("[k-ext1]Waiting event\n");
k_event_wait(tick_evt, CHAN_TICK, true, K_FOREVER);
printk("[k-ext1]Got event, giving sem\n");
k_sem_give(my_sem);
}
// (...)
First line allocates a kernel object, that will be our kernel event object. When we import the Zephyr C headers into Zig, we can access Zephyr symbols. So we should be able to replicate the first line in Zig with:
// (...)
const tick_evt: [*c]c.k_event = c.k_object_alloc(c.K_OBJ_EVENT);
// (...)
Note that *c is Zig way to define a C pointer. And the c. namespace before the Zephyr symbols is basically because we imported them to c:
const c = @cImport({
@cInclude("autoconf.h");
@cInclude("zephyr/kernel.h");
});
// (...)
If we try to build with the new line, we’ll get some errors, the first being:
(...)main.zig:9:11: error: unused local constant
const tick_evt: [*c]c.k_event = c.k_object_alloc(c.K_OBJ_EVENT);
^~~~~~~~
Errr... Zig really wants you to use stuff declared. While developing/debugging, this can be annoying. We can silence it with:
// (...)
const tick_evt: [*c]c.k_event = c.k_object_alloc(c.K_OBJ_EVENT);
_ = tick_evt;
// (...)
But there’s still a second error (now the only one left):
(...)main.zig:9:53: error: expected type '[*c]cimport.struct_k_event', found '?*anyopaque'
const tick_evt: [*c]c.k_event = c.k_object_alloc(c.K_OBJ_EVENT);
~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
(...)main.zig:9:53: note: pointer type child 'anyopaque' cannot cast into pointer type child 'cimport.struct_k_event'
(...).cache/zig/o/c45154c4214e553f112312a099f54b70/cimport.zig:3625:35: note: struct declared here
pub const struct_k_event = extern struct {
~~~~~~~^~~~~~
Of course. Zephyr k_object_alloc returns a void *, which Zig translates to ?*anyopaque. Which is basically an optional pointer to anyopaque. So we need to go from it to the C pointer to k_event. We can be tempted to simply cast it using @ptrCast(@alignCast()) construct, but that would land us in dangerous land - we’d be unwrapping the optional in an unsafe way. Instead we try:
// (...)
const obj: *anyopaque = c.k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]k_object_alloc failed!\n");
return 1;
};
const tick_evt: [*c]c.k_event = @ptrCast(@alignCast(obj));
_ = tick_evt;
// (...)
Here, we use orelse to handle the case when the optional is null. We add a debug message to help us in case we do face it, and return an error. We then build our extension with our ./build.sh and run it. We get horrified to see something like:
22:23:45.0626 [INFO] uart0: [host: 0.89s (+87ms)|virt: 43.1ms (+5.8ms)] *** Booting Zephyr OS build v4.2.0-2921-gb2273bd24342 ***
22:23:45.0634 [INFO] uart0: [host: 0.89s (+0.76ms)|virt: 44ms (+0.9ms)] [app]Subscriber thread [0x186c0] started.
22:23:45.0649 [INFO] uart0: [host: 0.89s (+1.55ms)|virt: 46.8ms (+2.8ms)] [app]Loading extension [kext1].
22:23:45.0683 [INFO] uart0: [host: 0.89s (+3.4ms)|virt: 55.9ms (+9.1ms)] [app]Thread 0x182a0 created to run extension [kext1], at privileged mode.
22:23:45.1507 [WARNING] uart0: Unhandled write to offset 0xC. Unhandled bits: [4, 6-7, 9-13] when writing value 0x3FFF. Tags: rxBreakDetectInterruptDisable (0x1), txFifoOverflowInterruptDisable (0x1), txFifoNearlyFullInterruptDisable (0x1), txFifoTriggerInterruptDisable (0x1), deltaModemStatusInterruptDisable (0x1), rxParityErrorInterruptDisable (0x1), rxFramingErrorInterruptDisable (0x1), txFifoFullInterruptDisable (0x1).
22:23:45.1508 [WARNING] ttc0: Unhandled write to offset 0xC. Unhandled bits: [5] when writing value 0x21. Tags: WaveformOutputDisable (0x1).
22:23:45.1524 [INFO] uart0: [host: 0.98s (+84.05ms)|virt: 61.8ms (+5.9ms)] *** Booting Zephyr OS build v4.2.0-2921-gb2273bd24342 ***
22:23:45.1530 [INFO] uart0: [host: 0.98s (+0.67ms)|virt: 62.6ms (+0.8ms)] [app]Subscriber thread [0x186c0] started.
22:23:45.1547 [INFO] uart0: [host: 0.98s (+1.64ms)|virt: 65.4ms (+2.8ms)] [app]Loading extension [kext1].
22:23:45.1583 [INFO] uart0: [host: 0.98s (+3.61ms)|virt: 74.6ms (+9.2ms)] [app]Thread 0x182a0 created to run extension [kext1], at privileged mode.
Repeating. What has happened? We dutifully enable log again at prj.conf (if we did disable it), and can see some interesting lines in the errors:
22:24:25.8563 [INFO] uart0: [host: 0.76s (+0.82ms)|virt: 0.13s (+1.5ms)] E: Undefined symbol with no entry in symbol table k_object_alloc, offset 32, link section 25
22:24:25.8580 [INFO] uart0: [host: 0.77s (+1.68ms)|virt: 0.13s (+1.7ms)] D: (invalid) relocation 2:3 info 0x290a (type 10, sym 41) offset 32 sym_name k_object_alloc sym_type 0 sym_bind 0 sym_ndx 0
22:24:25.8586 [INFO] uart0: [host: 0.77s (+0.64ms)|virt: 0.13s (+1ms)] D: (invalid) writing relocation type 10 at 0x31c20 with symbol k_object_alloc (0)
22:24:25.8594 [INFO] uart0: [host: 0.77s (+0.72ms)|virt: 0.13s (+1.5ms)] E: Undefined symbol with no entry in symbol table k_object_alloc, offset 32, link section 25
22:24:25.8596 [INFO] uart0: [host: 0.77s (+0.29ms)|virt: 0.13s (+0.4ms)] E: Could not find symbol k_object_alloc!
Wait - isn’t k_object_alloc a Zephy object? How can it not find it?
The answer is... not really. In fact, k_object_alloc is a system call, and they are handled in a particular way. System calls on Zephyr are inline functions that contain the architecture assembly code to trap into kernel mode. And this is the key here: assembly code. If we look into the .zig file created by Zig when importing the C headers1, we can find this:
// (...)
// (...)zephyr/llext-edk//include/zephyr/include/generated/zephyr/syscalls/kobject.h:75:22: warning: unable to translate function, demoted to extern
pub extern fn k_object_alloc(arg_otype: enum_k_objects) callconv(.c) ?*anyopaque;
// (...)zephyr/llext-edk//include/zephyr/include/zephyr/toolchain/gcc.h:626:2: warning: TODO implement translation of stmt class GCCAsmStmtClass
// (...)
So Zig did not generate code for those, as functions with assembly in it can’t be translated right now. Zig demotes then to extern functions, but Zephyr doesn’t have those. We’re out of luck...
But wait! We are running in kernel space with this extension, we need no syscalls! And that’s right, we don’t need them. When we invoke a syscall, Zephyr will first verify if we are running on kernel or userspace. If we’re in userspace, we trap into the kernel. But if we are already in kernel space, Zephyr simply calls the syscall implementation directly, which is basically the syscall with a z_impl_ prepended to its name2.
So let’s try calling the implementation directly:
// (...)
const obj: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return 1;
};
const tick_evt: [*c]c.k_event = @ptrCast(@alignCast(obj));
c.printk("[zig][k-ext1] Got tick_evt: %p\n", tick_evt);
// (...)
Let’s also print what we’ve got while at it. If we build and run, we can find this in the log:
22:49:43.7553 [INFO] uart0: [host: 0.83s (+0.24ms)|virt: 0.18s (+0.3ms)] [zig][k-ext1]Hello world!
22:49:43.7561 [INFO] uart0: [host: 0.83s (+0.31ms)|virt: 0.19s (+0.5ms)] [zig][k-ext1] Got tick_evt: 0x29060
We did it again! The next line in the C code should be simple to port now, just remember that k_event_init() is a syscall:
// (...)
c.z_impl_k_event_init(tick_evt);
// (...)
But the next one, the register_subscriber(), is more tricky:
// (...)
c.z_impl_k_event_init(tick_evt);
c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
// (...)
When we build the extension, we get:
(...)main.zig:18:6: error: root source file struct 'cimport' has no member named 'z_impl_register_subscriber'
c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
~^~~~~~~~~~~~~~~~~~~~~~~~~~~
The issue here is fairly simple, if we look at the Zephyr application code in this sample. The function register_subscriber is actually from the application, not from Zephyr. So we simply need to import its header file:
const c = @cImport({
@cInclude("autoconf.h");
@cInclude("zephyr/kernel.h");
@cInclude("app_api.h");
});
// (...)
But when we try to build the extension, we get:
(...)main.zig:19:33: error: value of type 'c_int' ignored
c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
(...)zephyr/samples/subsys/llext/edk/k-ext1/src/main.zig:19:33: note: all non-void values must be used
(...)zephyr/samples/subsys/llext/edk/k-ext1/src/main.zig:19:33: note: to discard the value, assign it to '_'
At least the error explain how to solve it. We won’t bother with the return of register_subscriber for now, so we go:
// (...)
c.z_impl_k_event_init(tick_evt);
_ = c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
// (...)
We can build it, and run. But there’s nothing new on the log. We get confident, and finish the while block - without the semaphore line, for now:
// (...)
_ = c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
while (true) {
c.printk("[zig][k-ext1]Waiting event\n");
c.z_impl_k_event_wait(tick_evt, c.CHAN_TICK, true, c.K_FOREVER);
c.printk("[zig][k-ext1]Got event, giving sem\n");
}
// (...)
And we build, to be welcomed by another error:
(...).cache/zig/o/4f85e05dbbbba36bf841646e76e8c727/cimport.zig:66715:34: error: unable to translate C expr: unexpected token '{'
pub const Z_TIMEOUT_TICKS_INIT = @compileError("unable to translate C expr: unexpected token '{'");
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WHAT?! There’s not even Z_TIMEOUT_TICKS_INIT in the code, what is this doing here? After we recompose ourselves, we can note that Zephyr’s K_FOREVER is a macro defined by a macro that eventually becomes Z_TIMEOUT_TICKS_INIT. Which is defined as:
#define Z_TIMEOUT_TICKS_INIT(t) {.ticks = (t)}
That {.ticks = (t)} doesn’t make any sense for Zig. One can see that it is a common idiom to initialise a struct in C, but which one? Luckily, we can figure it out: it is k_timeout_t. We may ask, “is this type already translated into Zig?”, and we look into cimport.zig:
// (...)
pub const k_timeout_t = extern struct {
ticks: k_ticks_t = @import("std").mem.zeroes(k_ticks_t),
};
// (...)
It is! It has a single field, ticks. By default, initialised to zero. But Zephyr K_FOREVER is actually -1. So we can create our own K_FOREVER in our Zig extension:
// (...)
pub const K_FOREVER = c.k_timeout_t{.ticks = -1};
// (...)
And use this instead in our wait call:
// (...)
_ = c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
while (true) {
c.printk("[zig][k-ext1]Waiting event\n");
_ = c.z_impl_k_event_wait(tick_evt, c.CHAN_TICK, true, K_FOREVER);
c.printk("[zig][k-ext1]Got event, giving sem\n");
}
// (...)
Note that we already ignored the return of z_impl_k_event_wait (we will be remembered by the compiler, anyway). After building and running, we can see some nice lines in the log:
(...)
23:27:45.6344 [INFO] uart0: [host: 2.1s (+0.38ms)|virt: 1.56s (+0.5ms)] [zig][k-ext1]Got event, giving sem
23:27:45.6346 [INFO] uart0: [host: 2.1s (+0.21ms)|virt: 1.56s (+0.3ms)] [zig][k-ext1]Waiting event
(...)
23:27:46.6344 [INFO] uart0: [host: 3.1s (+0.21ms)|virt: 2.56s (+0.5ms)] [zig][k-ext1]Got event, giving sem
23:27:46.6346 [INFO] uart0: [host: 3.1s (+0.26ms)|virt: 2.57s (+0.3ms)] [zig][k-ext1]Waiting event
(...)
23:27:47.6354 [INFO] uart0: [host: 4.1s (+0.49ms)|virt: 3.57s (+0.5ms)] [zig][k-ext1]Got event, giving sem
23:27:47.6356 [INFO] uart0: [host: 4.1s (+0.29ms)|virt: 3.57s (+0.3ms)] [zig][k-ext1]Waiting event
(...)
23:27:48.6368 [INFO] uart0: [host: 5.1s (+0.71ms)|virt: 4.57s (+0.5ms)] [zig][k-ext1]Got event, giving sem
23:27:48.6370 [INFO] uart0: [host: 5.1s (+0.27ms)|virt: 4.57s (+0.3ms)] [zig][k-ext1]Waiting event
(...)
Yay! The “giving sem” message is a bit of a lie, but our code to wait for events did indeed work!
Now, we have everything we need to reach feature parity with the C extension. We start by moving the event loop to a new function, tick_sub:
pub fn tick_sub(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
const obj: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return;
};
const tick_evt: [*c]c.k_event = @ptrCast(@alignCast(obj));
c.z_impl_k_event_init(tick_evt);
_ = c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
while (true) {
c.printk("[zig][k-ext1]Waiting event\n");
_ = c.z_impl_k_event_wait(tick_evt, c.CHAN_TICK, true, K_FOREVER);
c.printk("[zig][k-ext1]Got event, giving sem\n");
c.z_impl_k_sem_give(my_sem);
}
}
Some interesting points: we already added c.z_impl_k_sem_give(my_sem), but we’ll need to prepare it still. As this function is an entry point for the Zephyr thread, it needs the three pointers as parameters, but we won’t use them, so just name them _. And finally, we must not forget to use the callconv(.c), as it will be called from the C code.
On the main function, we now initialise the semaphore:
// (...)
const obj: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return 2;
};
my_sem = @ptrCast(@alignCast(obj));
_ = c.z_impl_k_sem_init(my_sem, 0, 1);
// (...)
Returning 2 just to differentiate. Now, initialise the other thread stack and... the thread:
// (...)
const sub_stack: [*c]c.k_thread_stack_t = c.z_impl_k_thread_stack_alloc(STACKSIZE, 0);
const obj2: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_THREAD) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return 3;
};
const sub_thread: [*c]c.k_thread = @ptrCast(@alignCast(obj2));
_ = c.z_impl_k_thread_create(sub_thread, sub_stack, STACKSIZE, tick_sub, null, null, null, PRIORITY,
c.K_INHERIT_PERMS, K_NO_WAIT);
// (...)
Here, K_NO_WAIT is similar to K_FOREVER, but we use the default initialiser - that makes ticks equals 0:
// (...)
pub const K_FOREVER = c.k_timeout_t{.ticks = -1};
pub const K_NO_WAIT = c.k_timeout_t{};
// (...)
For STACKSIZE and PRIORITY, we can simply write them:
// (...)
const STACKSIZE: c_int = 512;
const PRIORITY: u32 = 2;
// (...)
Finally, we declare my_sem:
// (...)
var my_sem: [*c]c.k_sem = undefined;
// (...)
It needs to be a var because we replace its value later. And we must initialise to undefined in Zig. The final result looks like:
const c = @cImport({
@cInclude("autoconf.h");
@cInclude("zephyr/kernel.h");
@cInclude("app_api.h");
});
const STACKSIZE: c_int = 512;
const PRIORITY: u32 = 2;
pub const K_FOREVER = c.k_timeout_t{.ticks = -1};
pub const K_NO_WAIT = c.k_timeout_t{};
var my_sem: [*c]c.k_sem = undefined;
pub fn tick_sub(_: ?*anyopaque, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
const obj: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return;
};
const tick_evt: [*c]c.k_event = @ptrCast(@alignCast(obj));
c.z_impl_k_event_init(tick_evt);
_ = c.z_impl_register_subscriber(c.CHAN_TICK, tick_evt);
while (true) {
c.printk("[zig][k-ext1]Waiting event\n");
_ = c.z_impl_k_event_wait(tick_evt, c.CHAN_TICK, true, K_FOREVER);
c.printk("[zig][k-ext1]Got event, giving sem\n");
c.z_impl_k_sem_give(my_sem);
}
}
pub fn start() callconv(.c) c_int {
const obj: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_EVENT) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return 2;
};
my_sem = @ptrCast(@alignCast(obj));
_ = c.z_impl_k_sem_init(my_sem, 0, 1);
const sub_stack: [*c]c.k_thread_stack_t = c.z_impl_k_thread_stack_alloc(STACKSIZE, 0);
const obj2: *anyopaque = c.z_impl_k_object_alloc(c.K_OBJ_THREAD) orelse {
c.printk("[zig][k-ext1]z_impl_k_object_alloc failed!\n");
return 3;
};
const sub_thread: [*c]c.k_thread = @ptrCast(@alignCast(obj2));
_ = c.z_impl_k_thread_create(sub_thread, sub_stack, STACKSIZE, tick_sub, null, null, null, PRIORITY,
c.K_INHERIT_PERMS, K_NO_WAIT);
while (true) {
var l: usize = undefined;
c.printk("[zig][k-ext1]Waiting sem\n");
_ = c.z_impl_k_sem_take(my_sem, K_FOREVER);
c.printk("[zig][k-ext1]Got sem, reading channel\n");
_ = c.z_impl_receive(c.CHAN_TICK, &l, @sizeOf(@TypeOf(l)));
c.printk("[zig][k-ext1]Read val: %ld\n", l);
}
return 0;
}
const StartSym = extern struct {
name: [*:0]const u8,
addr: *const fn() callconv(.c) c_int,
};
export const start_sym: StartSym linksection(".exported_sym") = .{
.name = "start",
.addr = start,
};
When we try building and running this final result, we are again greeted by infinite errors. Why? WHY? SERIOUSLY WHY?
A recurring theme during this exploration was that there’s very little memory available to work with on the simulator. Enabling logging could make things worse when trying to debug some error. By default, Zig generates a “Debug” build, with no optimisations and with safety checks. It’s fairly easy for the object file to grow a lot. For the whole sample, the kext1.llext could get to 28k. For comparison, the C one gets to mere 4k. To get parity in this aspect we’ll need to use -OReleaseSmall - it optimises for code size, and the safety checks are disabled. Change the build.sh to have:
zig build-obj -target thumb-freestanding-eabi $LLEXT_ALL_INCLUDE_CFLAGS -OReleaseSmall ../src/main.zig
With that, kext1.llext got to 2.3k, which is smaller that the C version - even with the code handling the null case for the optionals returned by k_object_alloc. Really impressive!3 Now, when we run it, we can see the joy:
(...)
02:55:35.5457 [INFO] uart0: [host: 0.73s (+0.81ms)|virt: 0.11s (+0.8ms)] [zig][k-ext1]Got event, giving sem
02:55:35.5461 [INFO] uart0: [host: 0.73s (+0.4ms)|virt: 0.11s (+0.5ms)] [zig][k-ext1]Got sem, reading channel
02:55:35.5463 [INFO] uart0: [host: 0.73s (+0.24ms)|virt: 0.11s (+0.3ms)] [zig][k-ext1]Read val: 0
02:55:35.5465 [INFO] uart0: [host: 0.73s (+0.19ms)|virt: 0.11s (+0.3ms)] [zig][k-ext1]Waiting sem
02:55:35.5468 [INFO] uart0: [host: 0.73s (+0.31ms)|virt: 0.11s (+0.4ms)] [zig][k-ext1]Waiting event
(...)
02:55:36.4056 [INFO] uart0: [host: 1.59s (+0.52ms)|virt: 1.08s (+0.7ms)] [zig][k-ext1]Got event, giving sem
02:55:36.4059 [INFO] uart0: [host: 1.59s (+0.37ms)|virt: 1.08s (+0.5ms)] [zig][k-ext1]Got sem, reading channel
02:55:36.4061 [INFO] uart0: [host: 1.59s (+0.2ms)|virt: 1.08s (+0.3ms)] [zig][k-ext1]Read val: 1
02:55:36.4063 [INFO] uart0: [host: 1.59s (+0.2ms)|virt: 1.08s (+0.2ms)] [zig][k-ext1]Waiting sem
02:55:36.4066 [INFO] uart0: [host: 1.59s (+0.3ms)|virt: 1.08s (+0.4ms)] [zig][k-ext1]Waiting event
(...)
02:55:37.4059 [INFO] uart0: [host: 2.59s (+0.7ms)|virt: 2.08s (+0.7ms)] [zig][k-ext1]Got event, giving sem
02:55:37.4064 [INFO] uart0: [host: 2.59s (+0.49ms)|virt: 2.08s (+0.5ms)] [zig][k-ext1]Got sem, reading channel
02:55:37.4067 [INFO] uart0: [host: 2.59s (+0.28ms)|virt: 2.08s (+0.3ms)] [zig][k-ext1]Read val: 2
02:55:37.4069 [INFO] uart0: [host: 2.59s (+0.23ms)|virt: 2.08s (+0.2ms)] [zig][k-ext1]Waiting sem
02:55:37.4073 [INFO] uart0: [host: 2.59s (+0.4ms)|virt: 2.08s (+0.4ms)] [zig][k-ext1]Waiting event
(...)
02:55:38.4073 [INFO] uart0: [host: 3.59s (+0.86ms)|virt: 3.08s (+0.7ms)] [zig][k-ext1]Got event, giving sem
02:55:38.4075 [INFO] uart0: [host: 3.59s (+0.25ms)|virt: 3.08s (+0.5ms)] [zig][k-ext1]Got sem, reading channel
02:55:38.4078 [INFO] uart0: [host: 3.59s (+0.3ms)|virt: 3.08s (+0.3ms)] [zig][k-ext1]Read val: 3
02:55:38.4080 [INFO] uart0: [host: 3.59s (+0.2ms)|virt: 3.08s (+0.2ms)] [zig][k-ext1]Waiting sem
02:55:38.4084 [INFO] uart0: [host: 3.59s (+0.4ms)|virt: 3.08s (+0.4ms)] [zig][k-ext1]Waiting event
(...)
02:55:39.4081 [INFO] uart0: [host: 4.59s (+0.7ms)|virt: 4.08s (+0.7ms)] [zig][k-ext1]Got event, giving sem
02:55:39.4088 [INFO] uart0: [host: 4.59s (+0.62ms)|virt: 4.08s (+0.5ms)] [zig][k-ext1]Got sem, reading channel
02:55:39.4088 [INFO] uart0: [host: 4.59s (+0.18ms)|virt: 4.08s (+0.3ms)] [zig][k-ext1]Read val: 4
02:55:39.4091 [INFO] uart0: [host: 4.59s (+0.21ms)|virt: 4.08s (+0.2ms)] [zig][k-ext1]Waiting sem
02:55:39.4095 [INFO] uart0: [host: 4.59s (+0.41ms)|virt: 4.08s (+0.4ms)] [zig][k-ext1]Waiting event
(...)
In the next installment, we’ll see what it takes to get an extension that runs on userspace to be written in Zig!
That file lives inside Zig cache. You can see its location revealed in some compilation error messages. ↩
If you want to see that in action, check <edk-install-dir>/include/zephyr/include/generated/zephyr/syscalls/kobject.h to see k_object_alloc implementation. ↩
You may be wondering if the C version did have the -Os flag to also optimise for space. It didn’t. If we rebuild it with -Os, kext1.llext gets to 2.5k - still bigger than the Zig version, which does a bit more. Really impressive indeed! And -Oz doesn’t make a difference from -Os in this case. ↩