A Dynamic Object System for Rust

Github Repository

I mentioned this in another thread and decided to follow through on sharing it.

Periodically a question pops up asking how to "do OOP" in Rust, and the answer is usually something along the lines of "it's technically possible, but you'd have to jump through a bunch of hoops so maybe don't." This made me wonder: "what would OOP look like in Rust?" which eventually led me to this small proof-of-concept library.

The library's object system is modelled loosely on the one in Objective C, and meets both the "common" definition of OOP (encapsulation, polymorphism, inheritance) and the "retcon" definition (encapsulation, message passing, late binding).

The core of the library is these two declarations:

pub trait ObjImpl {
	fn recv(&mut self, msg: *const u8, param: Obj) -> Obj;
	fn as_any(&mut self) -> &mut dyn Any;
}

#[derive(Clone)]
pub struct Obj {
	pub inner: Option<Rc<RefCell<dyn ObjImpl>>>
}

I basically just took all of Rust's "dynamic" features and tossed them into a single container. One major limitation is that objects lack "re-entrancy" i.e. you can't send a message to an object when it's already handling a message. I'm not sure how much that limitation matters in practice, though.

Idk if this is something anybody would like to use, but I thought it might stand as a nice discussion point at least.

1 Like

Creating SmallTalk on top of Rust is an interesting idea, although it might affect performance and compile-time checks (two things we like about Rust)

And if you take ObjC path, you might also need protocols: each object should be able to tell if it supports various messages, and some sugar not top of it. Otherwise, people would need to parse both message and params manually in each object.

Protocols add a bit of static typing to the Objective C object system but in this case Rust already has a pretty strong type system so adding something special for that seems like it'd be redundant. You could do this, for example, if you wanted an object with statically known capabilities:

msg! { msg_quack }

defclass! {
	struct DuckImpl {}
	fn new(){ DuckImpl {} }
	impl self, msg, _param {
		&msg_quack => String::from("quack quack ima duck"),
	}
}

trait Quacks { fn quack(&self) -> String; }

struct Duck { imp: Obj }

impl Duck {
	fn new() -> Self { Self { 
		imp: DuckImpl::new()
	} }
}

impl Quacks for Duck {
	fn quack(&self) -> String {
		self.imp.send(&msg_quack, Obj::nil()).to_string()
	}
}

That said, code that strips away all of the type information from an object only to add some of it back again in a wrapper object might get you some stares...

I'm not sure what you mean by "Otherwise, people would need to parse both message and params manually in each object." Are Objective C protocols more than a compile-time construct?

EDIT:
Also I think I should maybe clarify: that msg: *const u8 isn't a string that gets parsed or anything. It's literally just a pointer that the library uses as a unique identifier. You basically declare a static my_msg: u8 = 0; somewhere and the message is that static variable's address, sort of like symbols in LISP. There's no parsing that happens.