Sync vs async for library design

Hi,

I'm currently writing a library for interacting with UCI chess engines. The way UCI works is that the chess engine runs in a separate process and all communication is done via the engine process' stdin and stdout. This involves IO and potentially blocking code. Coming from a Javascript background, I am not too sure what kind of interface I should provide in my library in order to follow rust's best practices and keep my code fast.

1 - Should I provide a blocking or a non-blocking interface ?
2 - If I provide a non-blocking interface, should I use tokio's futures ? If not, what else ?

For the sake of simplicity, here is a very simplified version of my Engine struct. I currently have a blocking API.

pub struct Engine {
    process: std::process::Child,
    ...
}

impl Engine {
    // Computes the best move in the current position
    pub fn go(&mut self) -> Move {
        self.write("go");
        loop {
            match self.parse_line() {
                EngineMessage::BestMove(best_move) => return best_move,
                _ => {}
            }
        }
    }
    ...
}

A blocking API would be a fine starting point. This will keep your abstraction lightweight as you won't have to implicitly spawn a thread to watch the stdout from the chess engine process. You can then implement an asynchronous API on top of that or just leave it up to the user.

As a rule of thumb: use whatever interface best matches the underlying implementation.

If the process interaction is synchronous, then expose a synchronous API. If it's asynchronous, then expose an asynchronous one.

I agree with the above. Since what you're doing here sounds like it can reasonably be expected to be async, I'd suggest building on Tokio and Futures.

With that said, there shouldn't be anything stopping you from providing both types of APIs. You could have an EngineSync which uses the blocking loop you demonstrated as well as an Engine that can be used with Tokio's async functionality. Since picking up Tokio is far from trivial, you could start with a release featuring just EngineSync with the intention of eventually also supporting an async Engine. Hopefully by the time you get to the latter, your crate would be complete enough that you'd feel fairly comfortable factoring in a new type.

I'm not it's author, but if you'd like an example to consider, TRust-DNS took the approach I described above. As a user of TRust-DNS' client, I have found the transition from the single synchronous client to the new ClientSync (also synchronous) absolutely painless.

Another perspective, as someone who's presently working on a (primarily async) LDAP driver. Once you have an async version, it's not difficult to provide a synchronous adapter, but going in the other direction is far from trivial. That doesn't mean that you must start with async; as other posters have noted, a protocol is easier to reason about in sequential, synchronous code, up to a point.

If you choose to develop the async version, using the Tokio stack is probably the best course: almost all async work in Rust has converged to it, so it's easier to obtain assistance, and the result of your work (if you choose to publish it) is potentially usable by a wider audience.

2 Likes