-
Notifications
You must be signed in to change notification settings - Fork 0
DevFS
DevFS is the ModuOS device filesystem. It exposes kernel and driver resources as readable/writable files under the $/dev/ namespace. This is the NTOSIUX implementation of the POSIX "everything is a file" principle.
- Concepts
- Built-in Nodes
- Using DevFS from the Kernel
- Using DevFS from a SQRM Module
- Using DevFS from Userland
- API Reference
- Rules and Constraints
All DevFS paths start with $. This is how the kernel VFS (hvfs) distinguishes devfs paths from real filesystem paths:
$/dev/input/kbd0 ← DevFS node (keyboard)
/ModuOS/System64/sh.sqr ← Real filesystem path (ISO9660)
When userland calls open("$/dev/audio/pcm0", ...), the kernel routes it through the DevFS subsystem — not through the disk filesystem.
DevFS supports a tree structure with directories and leaf nodes:
$/dev/
audio/
pcm0 ← audio device
graphics/
video0 ← framebuffer
input/
kbd0 ← keyboard event stream
event0 ← raw input event stream
md64api/
pcname ← computer name (read-only text)
ram/
total ← total RAM in MB
available ← available RAM in MB
cpu/
model ← CPU model string
cores ← core count
Every registered node has an owner that controls who can overwrite it:
| Owner kind | When to use |
|---|---|
DEVFS_OWNER_KERNEL |
Core kernel code |
DEVFS_OWNER_SQRM |
SQRM driver modules |
DEVFS_OWNER_USER |
Userland-registered devices |
| Path | Description | R/W |
|---|---|---|
$/dev/input/kbd0 |
Keyboard — reads return Event structs |
R |
$/dev/input/event0 |
Raw input event stream | R |
$/dev/graphics/video0 |
Framebuffer info (md64api_grp_video_info_t) |
R |
$/dev/audio/pcm0 |
PCM audio output | W |
$/dev/md64api/pcname |
Computer name | R |
$/dev/md64api/osname |
OS name | R |
$/dev/md64api/version |
OS version string | R |
$/dev/md64api/kernelversion |
Kernel version string | R |
$/dev/md64api/arch |
CPU architecture | R |
$/dev/md64api/vm |
"1" if VM, "0" if physical |
R |
$/dev/md64api/vmvendor |
Hypervisor vendor name | R |
$/dev/md64api/datetime |
Current date/time as YYYY-MM-DD HH:MM:SS
|
R |
$/dev/md64api/sysinfo |
Full md64api_sysinfo_data_u binary struct |
R |
$/dev/md64api/ram/total |
Total RAM in MB (ASCII decimal) | R |
$/dev/md64api/ram/available |
Available RAM in MB (ASCII decimal) | R |
$/dev/md64api/cpu/vendor |
CPU vendor string | R |
$/dev/md64api/cpu/model |
CPU model name | R |
$/dev/md64api/cpu/cores |
Physical core count | R |
$/dev/md64api/cpu/threads |
Logical thread count | R |
$/dev/md64api/cpu/mhz |
Base clock in MHz | R |
$/dev/md64api/gpu/name |
GPU model name | R |
$/dev/md64api/gpu/driver |
Active GPU driver module | R |
$/dev/md64api/gpu/vram |
VRAM in MB | R |
This is for code inside the kernel itself (not a SQRM module).
#include "moduos/fs/devfs.h"static ssize_t my_read(void *ctx, void *buf, size_t count) {
my_device_t *dev = (my_device_t *)ctx;
// Copy data into buf, return how many bytes you wrote.
// Return 0 for EOF. Return < 0 for error.
const char *msg = "hello\n";
size_t len = strlen(msg);
if (len > count) len = count;
memcpy(buf, msg, len);
return (ssize_t)len;
}
static ssize_t my_write(void *ctx, const void *buf, size_t count) {
my_device_t *dev = (my_device_t *)ctx;
// Process the data written by userland.
// Return how many bytes you consumed.
(void)buf;
return (ssize_t)count;
}Rules for callbacks:
- Called from kernel context — no sleeping, no blocking.
-
ctxis whatever pointer you passed as the third argument todevfs_register_path. - Use
memcpyto copy to/frombuf— never dereference it as a userland pointer. - Return value follows standard
read()/write()semantics.
static devfs_device_ops_t my_ops = {
.name = "mydevice", // basename only, no path prefix
.open = NULL, // NULL = fine for most devices
.read = my_read, // NULL if write-only
.write = my_write, // NULL if read-only
.close = NULL, // NULL = nothing to clean up on close
.can_replace = NULL, // NULL = block all overwrite attempts
};void my_device_init(void) {
static my_device_t state; // your device state
devfs_owner_t owner = {
.kind = DEVFS_OWNER_KERNEL,
.id = "mydevice", // any identifying string
};
// Path is relative to $/dev/ — no leading slash, no $ prefix.
// Intermediate directories are created automatically.
devfs_register_path("mydevice", &my_ops, &state, owner);
}This creates $/dev/mydevice.
devfs_register_path("mydevice/info", &info_ops, NULL, owner);
devfs_register_path("mydevice/status", &status_ops, NULL, owner);
devfs_register_path("mydevice/data", &data_ops, &state, owner);This creates:
$/dev/mydevice/
info
status
data
Intermediate directories (mydevice/) are created automatically on the first devfs_register_path call that uses them.
If you need a separate state object for each open() call (e.g. a position cursor):
typedef struct {
size_t pos;
} my_open_ctx_t;
static void *my_open(void *ctx, int flags) {
(void)ctx; (void)flags;
my_open_ctx_t *oc = kmalloc(sizeof(my_open_ctx_t));
oc->pos = 0;
return oc; // this becomes the new ctx for read/write/close
}
static int my_close(void *ctx) {
kfree(ctx);
return 0;
}
static devfs_device_ops_t my_ops = {
.open = my_open,
.read = my_read,
.close = my_close,
// ...
};When .open is non-NULL, devfs calls it on every open() and passes its return value as ctx to all subsequent read/write/close calls for that fd.
Same API — only the owner kind changes.
#include "sqrm_sdk.h" // for sqrm_get_module_name() etc.
#include <moduos/fs/devfs.h>
static ssize_t my_read(void *ctx, void *buf, size_t count) {
// same as kernel version
return str_read("my sqrm device\n", buf, count);
}
static devfs_device_ops_t my_ops = {
.name = "mysqrmdev",
.read = my_read,
.write = NULL,
.open = NULL,
.close = NULL,
.can_replace = NULL,
};
int sqrm_module_init(void) {
devfs_owner_t owner = {
.kind = DEVFS_OWNER_SQRM,
.id = "my_module", // your module's name
};
devfs_register_path("mysqrmdev", &my_ops, NULL, owner);
return 0;
}If you want your SQRM module to take over a kernel-registered node (e.g. a GPU driver replacing the default stub), the kernel node must set can_replace:
// In the kernel stub:
static devfs_replace_decision_t gpu_can_replace(void *existing_ctx,
const char *path,
const char *new_owner_id) {
(void)existing_ctx; (void)path; (void)new_owner_id;
return DEVFS_REPLACE_ALLOW; // any SQRM driver may take over
}Then your SQRM module registers at the same path and it wins.
Userland uses standard POSIX file calls — no special API needed.
#include "libc.h" // ModuOS userland libc
// Read the computer name
int fd = open("$/dev/md64api/pcname", 0, 0);
if (fd < 0) { /* node not found */ }
char name[128] = {0};
int n = read(fd, name, sizeof(name) - 1);
close(fd);
// name now contains e.g. "DevmanPC"#include "libc.h"
#include <moduos/kernel/md64api_user.h> // md64api_sysinfo_data_u
int fd = open("$/dev/md64api/sysinfo", 0, 0);
md64api_sysinfo_data_u info;
read(fd, &info, sizeof(info));
close(fd);
printf("Total RAM: %llu MB\n", info.sys_total_ram);
printf("CPU: %s\n", info.cpu_model);int fd = open("$/dev/audio/pcm0", 0, 0);
write(fd, audio_samples, sample_count * 2);
close(fd);DevFS directories support opendir/readdir in the same way as real filesystem directories. The path must end without a trailing slash:
// Not yet implemented in userland libc — use known paths directly.int devfs_register_path(const char *path,
const devfs_device_ops_t *ops,
void *ctx,
devfs_owner_t owner);| Parameter | Description |
|---|---|
path |
Node path relative to $/dev/. Use / for subdirectories. No leading /. |
ops |
Pointer to your ops struct. Must remain valid for the lifetime of the node. Use static. |
ctx |
Your device state pointer. Passed to all callbacks. NULL is fine if you have no state. |
owner |
Who owns this node. Controls replacement policy. |
Returns 0 on success, -1 on failure.
typedef struct {
const char *name; // basename (e.g. "kbd0")
devfs_open_fn open; // optional — for per-open state
devfs_read_fn read; // ssize_t fn(void *ctx, void *buf, size_t count)
devfs_write_fn write; // ssize_t fn(void *ctx, const void *buf, size_t count)
devfs_close_fn close; // optional — for per-open cleanup
devfs_can_replace_fn can_replace; // optional — controls overwrite policy
} devfs_device_ops_t;typedef struct {
devfs_owner_kind_t kind; // DEVFS_OWNER_KERNEL, DEVFS_OWNER_SQRM, DEVFS_OWNER_USER
const char *id; // arbitrary identifying string (module name, driver name, etc.)
} devfs_owner_t;// Called on open() — return a per-open context, or NULL on failure.
// If NULL, the shared ctx is used directly.
typedef void* (*devfs_open_fn)(void *ctx, int flags);
// Called on read(). Return bytes copied, 0 for EOF, <0 for error.
typedef ssize_t (*devfs_read_fn)(void *ctx, void *buf, size_t count);
// Called on write(). Return bytes consumed, <0 for error.
typedef ssize_t (*devfs_write_fn)(void *ctx, const void *buf, size_t count);
// Called on close(). Return 0 on success.
typedef int (*devfs_close_fn)(void *ctx);
// Called when another owner tries to register at the same path.
// Return DEVFS_REPLACE_ALLOW to permit, DEVFS_REPLACE_DENY to block.
typedef devfs_replace_decision_t (*devfs_can_replace_fn)(
void *existing_ctx,
const char *path,
const char *new_owner_id
);-
Paths never start with
$or/when passed todevfs_register_path. The$/dev/prefix is added automatically. Just use"mydir/mynode". -
opsmust bestatic— the pointer must remain valid permanently. Never register a stack-allocated ops struct. - No sleeping in callbacks — callbacks run in kernel context. They may be called from any process or IRQ context. Use spinlocks if you have shared state.
-
No userland pointers — never pass
bufto functions that expect a kernel pointer without going throughusercopy_to_user/usercopy_from_user. -
ctxis shared unless you implement.open. If multiple processes open the same node simultaneously and you have no.open, they all share the samectx— make it thread-safe. -
Directories are implicit — you never create a directory explicitly. The first
devfs_register_path("foo/bar", ...)call createsfoo/automatically. -
No seeking — devfs fds do not support
lseek. If userland callslseekon a devfs fd, it returnsESPIPE.