Skip to content

Instantly share code, notes, and snippets.

@csherratt
Last active May 9, 2018 10:53
Show Gist options
  • Save csherratt/96aa8feaf501592b5969e62e11bece87 to your computer and use it in GitHub Desktop.
Save csherratt/96aa8feaf501592b5969e62e11bece87 to your computer and use it in GitHub Desktop.

Actors In Rust

Actors are a surprisingly useful abstraction, they are an alternative model that gives similar benefit as object ordinated (component interchangeably) but also helps address the concurrency issues that crop up with objects. Actors would could be a good solution to alleviating issues that crop up in building a UI framework in Rust.

This document proposes a hypothetical actor framework that I have been thinking about developing for Rust.

The Actor

An actor in this case is actually just a type that has an associated table of messages it can respond too. The Actor is boxed, and referenced counted to allow numerous agents to maintain a reference to the actor. Message routing is always dynamically issued, meaning there is only one entry point into the Actor framework to route to any actor. This is a performance trade off, we are exchanging performance (method lookup, boxing) for flexibility. Inside of an actor higher performance static dispatch can still exist to allow for lower overhead operations, but at the actor to actor interface we accept the performance tradeoff.

The basic actor is created using a compiler plugin, this creates vtables that are used for routing of messages to the actor.

// An actor is always just a structure.
struct Semaphore {
	count: u32
}

// this creates a message router for the actor.
messages! {
	actor Semaphore {
		// each function in the actor allows the actor to
		// perform any work it needs to do. The framework
		// acquires the actor automatically, if the method requires
		// mutable access this is done as a read/write lock
		fn up(&mut self) {
			self.count += 1;
		}

		// data can be return values just like normal methods in Rust
		fn down(&mut self) -> Bool {
			if down = self.count != 0;
			if down {
				self.count -= 1;
			}
			down
		}

		// methods do not require mutability can be invoked in a `Sync`
		// way. This means multiple external agents can grab the count w/o
		// locking the actor. 
		fn count(&self) -> u32 {
			self.count
		}

		// parameters can be passed into a method just like normal methods
		// in rust.
		fn set_count(&mut self, count: u32) {
			self.count = count;
		}
	}
};

messages! {
	// an interface can be declared, this allows for creation of
	// a vtable for access to the actor without having to worry about
	// missing methods.
	interface UpDown {
		fn up();
		fn down() -> Bool;
	}
};

fn main() {
	let actor = Actor::new(Semaphore{
		count: 1
	});

	// after the actor is created & wrapped, you can invoke
	// an of it's methods just like it was a normal object.
	// there is no need to lock / unlock the actor it is handled
	// by the framework.
	//
	// a macro is used for generic *any* style interfaces like this.
	actor.up();
	actor.down();

	// this will convert the actor into an typeless actor, this can
	// be used for dynamic typing purposes.
	let generic = actor.clone().any();


	// to send to an actor w/o using the static dispatch rout
	// the send macro can be used. This will invoke the actors'
	// method lookup before invoking the actor w/ the the correct method
	send!{generic.up()};

	//
	match send!{genric.down()} {
		Ok(tr) => {
			println!("{}", success)
		}
		Error(MessageError::Unimplemented) => {
			// dynamic dispatch allows for method to be absent.
		}
	}

	// using an interface we can avoid using the `send` macro.
	// if the interface could not be populated the 
	//
	// if an interface has a missing method the actor will raise
	// an error during the conversion process.
	let interface = generic.withInterface::<UpDown>().unwrap();
	interface.up();
	interface.down();

	// this will pin an actor to a specific thread. This is useful
	// for handing IPC to the main thread.
	let pinnedActor = Actor::thread_pinned(Semaphore{
			count: 1
	}, std::thread::current());

	thread::spawn(move || {
		// this will enqueue the actor to received a message on
		// the main thread. This thread will block until the message
		// has been accepted and the actor has finished processing it.
		pinnedActor.up();
	});

}
@naufraghi
Copy link

👍 Seems interesting, I like the seamless interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment