= FAT filesystem integration into MOP2 Kamil Kowalczyk 2025-11-19 :jbake-type: post :jbake-tags: MOP2 osdev :jbake-status: published Hello, World! I would like to present to you FAT filesystem integration into the MOP2 operating system! This was possible thanks to `fat_io_lib` library by UltraEmbedded, because there's no way I'm writing an entire FAT driver from scratch ;). Source for `fat_io_lib`: https://github.com/ultraembedded/fat_io_lib == Needed changes and a "contextual" system Integrating fat_io_lib wasn't so straightforward as I thought it would be. To understand the problem we need to first understand the current design of MOP2's VFS. === MOP2's VFS explained The VFS (ie. Virtual File System) is a Windows/DOS-style labeled VFS. Mountpoints are identified by a label, like for eg. `sys:/boot/mop2` or `base:/scripts/mount.tb`, which kinda looks like `C:\Path\To\File` in Windows. I've chosen this style of VFS over the UNIX kind, because it makes more sense to me personally. A mountpoint label can point to a physical device, a virtual device (ramsd) or a subdevice/partition device. In the UNIX world on the other hand, we have a hierarchical VFS, so paths look like `/`, `/proc`, `/dev`, etc. This is a little confusing to reason about, because you'd think that if `/` is mounted let's say on a drive `sda0`, then `/home` would be a home directory within the root of that device. This is not always the case. `/` can be placed on `sda0`, but `/home` can be placed on `sdb0`, so now something that looks like a subdirectory, is now a pointer to an entirely different media. Kinda confusing, eh? === The problem So now that we know how MOP2's VFS works, let's get into low-level implementation details. To handle mountpoints we use a basic hashtable. We just map the label to the proper underlying file system driver like so: [source] ---- base -> Little FS uhome -> Little FS sys -> FAT16 ---- Through this table, we can see that we have two mountpoints (base and uhome), which both use Little FS, but are placed on different media entirely. Base is located on a ramsd, which is entirely virtual and uhome is a partition on a physical drive. To manage such setup, we need to have separate filesystem library contexts for each mountpoint. A context here means an object, which stores info like block size, media read/write/sync hooks, media capacity and so on. Luckly, with Little FS we could do this out of the box, because it blesses us with `lfs_t` - an instance object. We can then create this object for each Little FS mountpoint. .Internals of the VFS mountpoint structure [source,c] ---- typedef struct VfsMountPoint { int _hshtbstate; char label[VFS_MOUNTPOINT_LABEL_MAX]; int32_t fstype; StoreDev *backingsd; VfsObj *(*open)(struct VfsMountPoint *vmp, const char *path, uint32_t flags); int32_t (*cleanup)(struct VfsMountPoint *vmp); int32_t (*stat)(struct VfsMountPoint *vmp, const char *path, FsStat *statbuf); int32_t (*fetchdirent)(struct VfsMountPoint *vmp, const char *path, FsDirent *direntbuf, size_t idx); int32_t (*mkdir)(struct VfsMountPoint *vmp, const char *path); int32_t (*delete)(struct VfsMountPoint *vmp, const char *path); // HERE: instance objects for the underlying filesystem driver library. union { LittleFs littlefs; FatFs fatfs; } fs; SpinLock spinlock; } VfsMountPoint; typedef struct { SpinLock spinlock; VfsMountPoint mountpoints[VFS_MOUNTPOINTS_MAX]; } VfsTable; // ... ---- .Little FS init code [source,c] ---- int32_t vfs_init_littlefs(VfsMountPoint *mp, bool format) { // Configure Little FS struct lfs_config *cfg = dlmalloc(sizeof(*cfg)); memset(cfg, 0, sizeof(*cfg)); cfg->context = mp; cfg->read = &portlfs_read; // Our read/write hooks cfg->prog = &portlfs_prog; cfg->erase = &portlfs_erase; cfg->sync = &portlfs_sync; cfg->block_size = LITTLEFS_BLOCK_SIZE; cfg->block_count = mp->backingsd->capacity(mp->backingsd) / LITTLEFS_BLOCK_SIZE; // left out... int err = lfs_mount(&mp->fs.littlefs.instance, cfg); if (err < 0) { ERR("vfs", "Little FS mount failed %d\n", err); return E_MOUNTERR; } // VFS hooks mp->cleanup = &littlefs_cleanup; mp->open = &littlefs_open; mp->stat = &littlefs_stat; mp->fetchdirent = &littlefs_fetchdirent; mp->mkdir = &littlefs_mkdir; mp->delete = &littlefs_delete; return E_OK; } ---- So where is the problem? It's in the fat_io_lib library. You see, the creators of Little FS thought this out pretty well and designed their library in such manner that we can do cool stuff like this. The creators of fat_io_lib on the other hand... yeah. Here are the bits of internals of fat_io_lib: [source,c] ---- //----------------------------------------------------------------------------- // Locals //----------------------------------------------------------------------------- static FL_FILE _files[FATFS_MAX_OPEN_FILES]; static int _filelib_init = 0; static int _filelib_valid = 0; static struct fatfs _fs; static struct fat_list _open_file_list; static struct fat_list _free_file_list; ---- There's no concept of a "context" - everything is thrown into a bunch of global variables. To clarify: THIS IS NOT BAD! This is good if you need a FAT library for let's say a microcontroller, or some other embedded device. Less code/stuff == less memory usage and so on. When I was searching online for a FAT library, I wanted something like Little FS, but to my suprise there are no libraries for FAT designed like this? Unless it's a homebrew OsDev project, of course. I've even went on reddit to ask and got nothing, but cricket noises ;(. I've decided to modify fat_io_lib to suit my needs. I mean, most of the code is already written for me anyway, so I'm good. The refactoring was quite easy to my suprise! The state of the library is global, but it's all nicely placed in one spot, so we can then move it out into a struct: [source,c] ---- struct fat_ctx { FL_FILE _files[FATFS_MAX_OPEN_FILES]; int _filelib_init; int _filelib_valid; struct fatfs _fs; struct fat_list _open_file_list; struct fat_list _free_file_list; void *extra; // we need this to store a ref to the mountpoint to access storage device hooks }; ---- [source,c] ---- // This is what our VfsMountPoint struct from earlier was referencing, BTW. typedef struct { struct fat_ctx instance; } FatFs; ---- I've then gone on a compiler error hunt for about 2 hours! All I had to do is change references for eg. from `_fs.some_field` into `ctx->_fs.some_field`. It was all pretty much brainless work - just compile the code, read the error line number, edit the variable reference, repeat. == Why do I even need FAT in MOP2? I need it, because Limine (the bootloader) uses FAT16/32 (depending on what the user picks) to store the kernel image, resource files and the bootloader binary itself. It'd be nice to be able to view all of these files and manage them, to maybe in the future for eg. update the kernel image from the system itself (self-hosting, hello?). == Fruits of labour Here are some screenshots ;). image::/img/fatfs-showcase2.png["showcase 2"] image::/img/fatfs-showcase1.png["showcase 1"]