#BadgerOS
1 messages · Page 3 of 1
Moment of truth: In which way will the new VMM horribly crash?
I've been using page numbers but that's been causing so many problems I'm strongly considering converting everything to page-aligned addresses (for both virtual and physical)
Yeah okay that's it
(It fucks with the sign bits)
for VAs?
Both virtual and physical addresses in the entire memory management schtick were page numbers (i.e. actual address divided by PAGE_SIZE)
ig you could use a signed right shift
But you get sign extension memes if you do that for virtual addresses, which I don't feel like dealing with.
But I'll fix that tomorrow, already been programming all day
About halfway through that conversion
But it is very tedious because I need to manually review every instance of interacting with the VPN and PPN types
(Which have been removed)
But now there's still code that thinks the API talks in pages, so I need to change that too
I am manually interpreting page tables once again
Getting closer, page tables good enough to run in but calling virt2phys causes a load access fault so I suspect there's still something wrong with the page tablage
It thinks a page table has physical address 0, which I suspect may be due to incorrect flags?
Turns out it was mapping things in the same spot twice because I wasn't keeping my list sorted
Now it fails when trying to interact with the SATA drive, thinking it has a sector size of 0
Another bug located: PhysMap wasn't actually unmapping things correctly, causing later mappings to be overriden by long-gone ones.
Incremental progress... The new process segfaults accessing 0x0 after fork()
Still slowly squashing bugs... VMM seems mostly stable, but has revealed a set of what are likely unrelated bugs.
After that I want to implement file window mappings
When I have that I'll be satisfied with VMM for the moment.
Final bugs squashed there, very nice. Also fixed an init-order problem that caused devices not to have their interrupts enabled if the interrupt controller was later down the device tree.
Now that the kernec has enough features to support it, I want to port more stuff. In particular a c++ library, readline and a GNU toolchain
because if I have GCC working then I can get my sysdeps upstreamed
Just switched my syscalls over to a new system that automatically generates the marshalling. This will make it easier to add new syscalls and also make the marshalling logic less error-prone because less of it is manually written.
Though while testing other things I keep running into the device subsystem being horrifically broken. Time to finally re-write all that as well.
Once again I realize that Rust's type system is just NOT ideal for the kind of device model I want.
Without class-style inheritance, I must either make an "is XYZ" (e.g. fn is_char(&self) -> Option<&dyn CharInterface>) method for every type of non-bus interface (e.g. interrupt controller interrupts, character API for serial drivers, etc.) or require a struct-based abstraction, requiring wrappers in places where you would otherwise not need them (e.g. struct CharInterface(Arc<dyn CharInterfaceTrait>) and then Arc<CharInterface>)
If I had class-style inheritance I could simply have used a composition-based model where you have e.g. Ns16550a : CharDevice and CharDevice : Device
And, notably, in languages where you have this style of composition, you can usually explicitly check if it is a CharDevice without checking first whether it is a Ns16550a
All of this combined means you can't easily create a centralized device subsystem that supports registering device types with modules (i.e. types not baked directly into the kernel)
And you can forget about the idea of multiple levels of inheritance (e.g. choosting to do Ns16550a : TtyDevice, TtyDevice : CharDevice and CharDevice : Device)
fwiw we never have this kind of inheritance pattern for devices in Managarm so it's definitely not mandatory
Really, if we're talking language considerations then this is a huge con for Rust (given the model I want) compared to C++
Oh I know
But I want to represent it this way
you can make it work pretty well
step 1 dont use arc
Especially considering I chose Rust for its type system (not necessarily the borrow checker)
yeah something like that
No, because you can't use Any to get a trait.
Because Rust vtables are lazy
you can obviously copy paste that once for each kind of vtable
and use a macro to make it like not an issue
and you can use try_as_dyn (nightly) to get a vtable if the trait is implemented (statically ofc)
So you have to check for the specific implementation of the trait, which defeats the point of this
This would only return the vtable if something already explicitly asked for that specific device with that specific trait combination
you can do something like this: ```rs
pub struct BasicallyArc<T> {
vtable: &'static Vtable,
ptr: NonNull<()>, // actually T
marker: PhantomData<fn(T) -> T>, // invariant
}
struct Vtable {
trait_1: Option<DynMetadata<dyn Trait1>>, // repeat for all of the traits you have
}
idk i dont see an issue, i have a (very simple) inheritance-style hierarchy in my kernel for objects (objects can be an Object or VMObject, where VMObject subtypes Object) and you could totally extend the same idea to other things
I'm not saying it's impossible to do inheritance
That's just a bit more manual
I'm saying it's infeasible to make a system where by trait alone you look for a device
yeah i disagree
So the least-worst option (imo) would be a system where you explicitly add a is_xyz() for every device type
Because I don't want to fight the type system while doing this (which this impl very much is imo)
there is zero type system fighting here imo
It also doesn't solve device types defined by modules alone (completely unknown to the kernel before the module is loaded)
Though I can drop that requirement
okay so what do you even want to inherit
thats like the main question i think
because if you want to support diamond inheritance and all of the really stupid shit you can in c++ thats pretty annoying
but ime thats not very common
like your example is literally two levels of inheritance
and its two interface-only levels too pretty much
and that is actually quite simple
and idk how common is it to have a device which provides two distinct kinds of thing
Well let's use an NS16550A-compatible (non-diamond inheritance btw) driver example:
Somewhere you have a device that implements a trait InterruptController.
Then you have an MmioBus (NOT a device, DTB-discovered) which references its interrupt controller
And finally you would have your Ns16550a which implements e.g. CharDevice (and in turn Device).
The device registry would store Arc<dyn Device> so now you have a situation where both your InterruptController and CharDevice traits have been erased from the question
yeah so you need some way of going back
but thats not really all that hard
you can obviously have a as_[whatever trait] function per trait
which is a bit annoying but totally managable
Yes. And I keep saying that this needs a pattern I find ugly (sorry, your BasicallyArc is ugly) or to explicitly have checks in the Device trait for each subtype (and maybe some Arc casting helpers). Again, it can be done but is inferior to C++'s approach (ignoring diamond inheritance)
yeah the internals of this kind of setup dont look great
the basicallyarc approach isnt really that ugly imo
especially if you dont look at the internals 
And either way I can forget about module-defined traits
no
you can define as_trait(typeid: TypeId), and then to downcast to dyn Foo, typeid is TypeId::of::<dyn Foo + 'static>()
and you can wrap it in a nice macro or with some helpers
there are other ways of generating IDs too:
- if you can have a macro per-interface-definition, you can create a global there and use that as the ID
- if your inheritance path is always one level at most (which is pretty common in practice), its even easier because you only need a single field for "top level subtype"
- you can obviously just accept not having interfaces in modules, its not like required (and how many modules actually end up declaring interfaces anyway)
An alternative solution could be to have a fn register_interface<T: Device + 'static>(...) and a matching search of some kind in the registry that does the TypeId hacks internally
sure, or a static or whatever else
(Actually the trait Device should probably just have the lifetime bound 'static)
yes
i mean personally i wouldnt use Arc at all because it requires infallible oom handling so its kinda bad anyway
and if you are building your own refcounted types there is really no issue in having your own magic header/vtable state for doing more complex downcasting behaviors
because it requires infallible oom handling
Only sort of. You can try_new with them. And they're convenient for refcounting because every type that refcounts itself needs even moreunsafeblocks.
and like if your device model is more complex than just "lol arc"
oh does that work with arc
yes
idk it didnt work with vec or string so i decided to ignore it all
but there isnt a try_push
and theres no way im remembering to use try_reserve every time lmao
my impl is that i have something like Arc<Object<T>> where T is the specific object type, or AnyObject for a generic "some object" entry
except its my own arc type that can be told to do whatever
Though the discussion of std types for Box, Arc, Vec, etc. is a different one imo. If I were doing my own thing with these (which I might in the future if I end up doing actual OOM handling) then I'd still make my own replacements with largely the same API instead of putting the refcount directly in the type being refcounted instead of the smart pointer's wrapper.
but yeah thats like not the point anyway
I think register_interface is probably the cleanest solution here. And when a hot-pluggable device is removed you'd end up having to do a bunch of remove_ / uninstall_ calls anyway so not like that matters too much.
i would still make custom refcounting types for devices tho
because yeah the funny type casting required is a bit hard to do otherwise
so you'd have a DeviceHandle<T> and AnyDeviceHandle for example
in my case i have Ref<Object<T>> and Ref<Object<AnyObject>> (where AnyObject is implied)
And those implement refcounting for the devices with some generic struct every device has to fit into like BaseDevice<T> or something?
but you could also totally have Ref<Object<dyn Foo>>
yes, or you can do the same thing that arc does
i just have a repr(C) wrapper struct tho bc easier
this is going to need more thought than I have energy for right now
also its possible to implement without unsafe (except in the refcounting pointer impl)
via field_projections
or with hacky macros
which allows you to write an object like ```rs
pub struct Project<Base, Type> { .. }
what are field projections
surely you can use Arc on nightly w/o non-failable allocs?
it adds a magic macro called field_of!() which lets you name the type of a field
yeah you can use arc
you just cant use vec or string without really ugly APIs
A specific kind of ID that is for a field within some type?
that make it less ugly
its some unknown type which implements the Field trait
which tells you what the base and target type is and the offset
its also possible to write it on stable with some slightly ugly macro hacks
well you can use it to implement safe internal refcounting
Yeah but I use unstable Rust for many things already
its actually quite handy as well it also lets you write slightly more fun intrusive collections
and it can make raw pointers cleaner
Well either way I'll have to come back to this discussion again later
Because I've now spent over 3 hours doing nothing but thinking of this and closely related stuff
I'll make sure to ping you next time it comes up :^)
Also my nightly Rust seems to predate this macro sooo... that would mean updating whatever breaks
lol
nah if you just need single inheritance you can do that with some slightly cursed macros
i'm still for languages having a meta layer
smh just make a custom programming language at this point 
drivers are too high-level for most low-level languages
a metalayer would make the DX even worse though
only C++ has really achieved something like a language with an integral metalayer but it's frightfully complex and not as meta as objective-
also not exactly sure what you mean by this
stuff like reflection?
and objects, messaging, generally flexibility, dynamism, and late-binding
Can you link me something about this meta-stuff to read up on
or point me in a general direction
marcel weiher has a lot of insight into it https://blog.metaobject.com/2024/08/objective-c-is-just-like-leaky.html
he talks about it quite often on his blog
he is drawing on something that's had a long history though, he talks about the "two language problem" which is a similar problematic (you'll note that two-language situations are everywhere, python + libraries written in C or Fortran is a good example)
BadgerOS is always going to be two language because uACPI
for example
actually flanterm too
they're equivalent languages so it doesn't count much there
it's the dynamic + static language combo that's more interesting
and I'm taking that I'm running into the fact Rust is static but the driver model I want is dynamic
The first time I heard Jimi Hendrix I had no idea what all the fuss was about. Sure, it was great, but it wasn't changing my life. Maybe a decade later, it hit me: thirty years previous, he'd changed every guitarist's life and changed the sound of guitar music forever. He was the first person to play a guitar like that but it happened before I was born, so I just accepted the post-Jimi guitar sound as normal and nothing special.
I'm getting some sense that Objective-C is like that. Books, manuals and grey-bearded Objective-C programmers bang on about how marvellous the runtime is and the power of code introspection and method swizzling and dynamism and I'm thinking, "Yeah, so what?". Even as a Java programmer[1], I just expect my programming environment to have those features.
But as a C programmer in the 1980s, this stuff must have been fucking mindblowing! Having joined the industry in the late 90s, the languages I've used have been mostly paving over the trails blazed by Objective C (...
jimi hendrix mention
This whole schtick about objc is actually exactly what I needed to hear
Because it's true: I want something that reeks of dynamic typing in a statically-typed language
The struct of shenanigans in the registry:
/// A device as stored in the registry.
struct Entry {
/// Raw [`Arc`] pointer.
ptr: *const (),
/// The type ID of the interface trait this provides, which may be [`Device`].
trait_id: TypeId,
/// The v-table for [`Device`].
base: *const (),
/// The v-table for the trait named by [`Self::type_id`].
specialized: *const (),
}
why const ()
Just arbitrary pointer
well base is always DynMetadata<dyn Device> right?
Because this is a decomposed arc
technically yes, in practice I thought it'd look ugly :^)
and all of the pointers can be nonnull<()> in general
That's kinda pointless because I'd have to turn it into a NonNull and back everywhere
Just duplicates the amount of random BS
#![feature(ptr_metadata)]
use core::{
any::Any,
any::TypeId,
ptr::{DynMetadata, Pointee, null},
};
use std::sync::Arc;
/// An abstract device.
/// While some common logic is enforced for all devices, most of the logic depends on their specific types.
pub trait Device: Any + Send + Sync + 'static {
// ...
}
/// Register a new ...
I have run into a problem
That ?Sized relaxation is needed to actually call to e.g. register_device::<dyn CharDevice>(...) but it also prevents the cast to dyn Device pointer types
you have to implement Unsize and CoerceUnsized i think
ah no this is different
oh hmm yeah this is kinda tricky
I need the compiler to emit that cast so I have the base metadata pointer
But I don't see how
i mean the way i would approach it is to construct my own vtable in a fixed object header
and then you kinda dont have this class of issue
I'm so close
If only I could inform the trait solver that that cast has to be possible too
yo
that was it
Well that and being more specific that I don't want it to unsize in some specific spots
hmm
i wonder
you can assert that the unsized version has the same metadata theoretically
i wonder what the actual layout is
You don't have to, the bounds I placed already do this
The fact that I unsize it there forces the coercion to dyn Device and every pointer of dyn Device will have metadata be DynMetadata<dyn Device>
Fianlyl fixed the DTB parser
Gonna play around with exactly how I want DTB-based probing to work
But the current thing seems to work in that regard.
The new model in use for associating interrupt controllers per CPU needs at least a small amount of tweaking still to support x86's model
Because for that to work, you need a CPU irqno -> device map whereas the current one is a blanket CPU -> device map
In my new device subsystem, I want to use a concept called "busses" (loosely adapted from nubs in IOKit). The idea is that a bus is a location where one device can connect, so that drivers which attach to buses can be appropriately probed (either immediately or delayed for drivers loaded later on).
The question: How do I handle multiple bus types?
For example, there will be very generic buses like MMIO and PCIe, but potentially also something like an abstract ATA bus (which would let the same ATA driver be used for multiple interfaces that use it under the hood).
I've considered using a trait Bus: Any so that a specific specialization can be found at runtime, but I think this is somewhat clunky and it also doesn't work if the specialization is itself a trait.
Another I've considered is using an enum Bus { MmioBus(MmioBus), PciBus(PciBus), ... }; this would make it very explicit which types of bus exist, but also make the implementation of it slightly more rigid. Do note that because of how I handle devices, you could still feasibly have multiple implementers of ATA by simply moving that trait onto the device itself (e.g. trait AtaControllerDevice: Device.
What do you think? Maybe a third option I've missed?
i wouldn't call MMIO a bus
it's way too generic
pcie yes very much
most devices are behind some kind of bus
#polls message
Well for MmioBus it would in reality mean a DTB node that lives in /soc
Details, details
Alright so @solid bronze I've decided: Bus will be a plain trait. It happens to be dyn compatible but the devices are gonna downcast it to whatever specific bus type they want for the extra functions.
Doing the ops thing for devices is still under consideration as it would indeed allow arbitrary expansion of functionality, but a concrete implementation with it needs some more details sorted out first.
Especially considering there would almost always be two nested levels of these ops: the first is the subclass and the second is implementation.
Returns Some(&U) if T can be coerced to the trait object type U. Otherwise, it returns None.
@fading elm did you see this
(nightly)
No but this is still compile time only
Been working on the interrupt bit of the new device subsystem. Not sure just how lenient I should be for strange DTB configurations. Do I allow random devices connected directly to the CPU interrupt controller? Especially because CPU interrupt controllers aren't proper devices but instead an arch implementation detail.
I'm probably going to check how lenient Linux is in this regard
Slightly more progress once again, need to create a thread that probes buses for drivers as either become available. But that needs a new way for threads to block I hadn't made yet, so looks like scheduler detour time.
On DTB this is for literally everything, including the PLIC for example, but on ACPI systems some of that is informed by the MADT instead, where devices such as IOAPICs and the PLIC are directly instantiated in the MADT parsing code.
Seems like this new device subsystem concept is actually going to work after all
that must be satisfying, I feel like you've been working on the design for some time now.
Very long, yes
Devices are one of the things I wanted to experiment with doing better
I could've easily done something simpler.
Progress!
@solid bronze I've finally solved the dyn Device problem:
pub trait Device: Any + Send + Sync + 'static {
/// Test whether this device implements a trait and get its metadata if so.
/// Should not be used directly.
fn get_trait_vtable(&self, trait_: TypeId) -> Option<DevDynMetadata>;
}
impl dyn Device {
pub fn try_as_ref<T: ?Sized + Pointee<Metadata = DynMetadata<T>> + 'static>(
&self,
) -> Option<&T> {
let meta = self.get_trait_vtable(TypeId::of::<T>())?;
unsafe {
let ptr: *const T = core::ptr::from_raw_parts(
self as *const dyn Device as *const (),
core::mem::transmute(meta),
);
Some(&*ptr)
}
}
}
holy shit
You use the Any to get the specific implementation if you know it, or use a kinda hacky function to get the VTable as a trait of choice
Helper macro means all you do in impl is write device_get_trait_vtable!(IrqCtlDevice) for example
So you just list traits you want to advertise
So how cursed do you rate it out of 10 :P
9
lolololol
Oh well
It's only one place where you actually see how jank it is
I don't think there was ever gonna be a non-jank solution to the exact kind of thing I wanted to do anyway
i wonder how linux does sysfs interactions in rust
Well, again, I'm trying to do dynamic programming in a static language
There was never going to be an idiomatic solution
I'm probably going to finally implement hugepages after this, because I'm starting to get sick of waiting so long for the HHDM to build on startup (which is made worse by Rust debug builds)
With the whole device subsystem coming together nicely, it's time to port the PICe controller driver and the AHCI driver
PCIe enumeration is working, now going on a small detour to get me an ID allocator and a mechanism to make sure only one device can use a bus at a time
@slate osprey what is the laughing react emoji on this post for
fat finger
Well, s/port/rewrite/ I guess. I'm making the SATA driver a lot more proper than I had last time.
It'll actually be interrupt-driven, with a driver thread per port, and it isn't implementing BlockDevice directly but rather exposes an AtaBus and I'll have a separate impl of BlockDevice for that.
I have the basic loop for the port done, now time to build me an AtaBus and try sending some identification commands
@trim harness i am now shilling BadgerOS to you
Nice nice
I didn't even notice you weren't in this thread
I prolly was at one point til the incedent™
So that's a new one...
My limine base revision stopped working...
Care to know why?
Because a string table somewhere contains the same magic 
So I'm going to move the requests earlier into the binary, and into their own phdr
How's that even possible, the magic constants aren't normal ASCII
shouldnt the requests be near the top too anyways?
And you dont need a phdr, just a section is enough to order them
phdr anyway bec i say so
I had put them in rodata
Which is near the end lol
Now they're the very first thing in the kernel

Anyway I'm finally switching over to my Rust Limine bindings
Fucked smth up in HHDM calculation I think
but anyway soon™ there will be no more C
There are three C parts the kernel depends on:
- malloc
- old device subsystem
- boot protocol
And two of them I am actively working on replacing, leaving only malloc.
I happen to already have a suitable slab allocator written in Rust so I can just steal that one.
Next up is definitely hugepages
Because Rust is undebuggable with any opt on
But that also means that only very basic opt happens at all
And because of the large number of layers in atomics, the pmap code is super slow while creating the hhdm
-O1 is minimum
Well there are some things I just can't properly debug even at -O1
While testing device rewrite, I found two unrelated bugs, the first of which likely a regression:
- In specific circumstances, an async signal causes a segfault in the target
- Using SIGKILL doesn't actually stop the userland thread, causing a half-dead process to run forever
BadgerOS now supports hugepages, mainly used for the HHDM atm
Leaving space for things like page caches to support them in the future.
Working on VFS again, going to add support for remounting so that e.g. all devtmpfs have the same instance
This involves changing the mounts table
Also needs significant changing to umount as there is now more need to check what is open
Dump these in your folder as .config:
how tf do i point it to a different C compiler
you need to edit all
leftover from my earlier, very shitty, "build system"
I'm fixing it after I finish removing C
What? You think I'm stupid enough not to pin my nightly?
no the problem is that you didn't include rust-src 
$ cat repos/zinnia/sources/zinnia/rust-toolchain.toml
[toolchain]
channel = "nightly-2025-12-30"
components = ["rust-src", "rust-analyzer", "rustfmt", "clippy"]
(i have a rust port so it doesn't matter for me anymore, but consider doing this so it pulls that automatically)
will do
ok i think i have a kernel
now dump it on <insert limine riscv box>
-O0 + not stripped = big
hmmmmmmmmmmmmmmm
I figured it out
Limine base revision must be before the start marker it appears
huh
wait no
I checked the VM memory by the way
Limine has not modified the actual base revision marker
And the magic checks out
i have a theory of what's happening
let me check it
i see the problem
the markers get removed because of dead code elim
you need to mark them as used
ffs
the start marker only ever shows up in the segment