Skip to content
NtinosTheGamer2324 edited this page Mar 7, 2026 · 1 revision

DevFS — Device Filesystem

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.


Table of Contents

  1. Concepts
  2. Built-in Nodes
  3. Using DevFS from the Kernel
  4. Using DevFS from a SQRM Module
  5. Using DevFS from Userland
  6. API Reference
  7. Rules and Constraints

Concepts

The $ prefix

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.

Nodes and directories

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

Owners

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

Built-in Nodes

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

Using DevFS from the Kernel

This is for code inside the kernel itself (not a SQRM module).

Step 1 — Include the header

#include "moduos/fs/devfs.h"

Step 2 — Write your read and write callbacks

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.
  • ctx is whatever pointer you passed as the third argument to devfs_register_path.
  • Use memcpy to copy to/from buf — never dereference it as a userland pointer.
  • Return value follows standard read()/write() semantics.

Step 3 — Fill in the ops struct

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
};

Step 4 — Register

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.

Creating a subtree

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.

Using open for per-open state

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.


Using DevFS from a SQRM Module

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;
}

Replacing a kernel node from a SQRM module

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.


Using DevFS from Userland

Userland uses standard POSIX file calls — no special API needed.

Reading a text node

#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"

Reading the binary sysinfo struct

#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);

Writing to a writable node

int fd = open("$/dev/audio/pcm0", 0, 0);
write(fd, audio_samples, sample_count * 2);
close(fd);

Listing a DevFS directory

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.

API Reference

devfs_register_path

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.


devfs_device_ops_t

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;

devfs_owner_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;

Callback signatures

// 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
);

Rules and Constraints

  • Paths never start with $ or / when passed to devfs_register_path. The $/dev/ prefix is added automatically. Just use "mydir/mynode".
  • ops must be static — 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 buf to functions that expect a kernel pointer without going through usercopy_to_user / usercopy_from_user.
  • ctx is shared unless you implement .open. If multiple processes open the same node simultaneously and you have no .open, they all share the same ctx — make it thread-safe.
  • Directories are implicit — you never create a directory explicitly. The first devfs_register_path("foo/bar", ...) call creates foo/ automatically.
  • No seeking — devfs fds do not support lseek. If userland calls lseek on a devfs fd, it returns ESPIPE.

Clone this wiki locally