Async/await around a blocking C library

Forgive me if this is obvious, but I'm very new to async/await. If this topic is covered in some resource online, I'd greatly appreciate a pointer to it.

I'm writing a Swift wrapper around libmodbus, which I'm currently calling like this:

	public
	func
	readRegister(address inAddr: Int, fromDevice inDeviceID: Int, completion inCompletion: @escaping (UInt16?, Error?) -> ())
	{
		self.workQ.async
		{
			do
			{
				self.deviceID = inDeviceID
				let r = try self.readRegister(address: inAddr)
				self.callbackQ.async { inCompletion(r, nil) }
			}
			
			catch (let e)
			{
				self.callbackQ.async { inCompletion(nil, e) }
			}
		}
	}

	func
	readRegister(address inAddr: Int)
		throws
		-> UInt16
	{
		if self.deviceID == -1
		{
			throw MBError.deviceIDNotSet
		}
		
		var v: UInt16 = 0
		let rc = modbus_read_registers(self.ctx, Int32(inAddr), 1, &v)
		if rc != 1
		{
			throw MBError(errno: errno)
		}
		return v
	}

The libmodbus structure is only ever accessed on the workQ, and the callbacks always happen on the callbackQ. Other than that, each individual libmodbus call blocks until it completes (or times out).

What's the best way to wrap a library like this in a new async/await-style API?

A simple but slightly suboptimal solution would be to just wrap the whole thing in an actor. Blocking actor threads isn't great, but blocking just one actor thread is generally safe enough (I guess it's a problem if you ever ship on a single core device).

More robust would be to use withCheckedThrowingContinuation with a callback queue like you currently have.

Eventually, once we have custom executors, that would be a way to fix the downsides of the first approach.

3 Likes

To close the loop on this, this is how I ended up implementing it:

	public
	func
	readRegister(fromDevice inDeviceID: Int, atAddress inAddr: Int)
		async
		throws
		-> UInt16
	{
		try await withCheckedThrowingContinuation
		{ inCont in
			self.workQ.async
			{
				do
				{
					self.deviceID = inDeviceID
					let r = try self.readRegister(address: inAddr)
					inCont.resume(returning: r)
				}
				
				catch (let e)
				{
					inCont.resume(throwing: e)
				}
			}
		}
	}

Annoyingly, Void return types needed explicit typing:

	public
	func
	write(toDevice inDeviceID: Int, atAddress inAddr: Int, value inVal: Float)
		async
		throws
	{
		try await withCheckedThrowingContinuation
		{ (inCont: CheckedContinuation<Void, Error>) -> Void in
			self.workQ.async
			{
				do
				{
					self.deviceID = inDeviceID
					try self.write(address: inAddr, value: inVal)
					inCont.resume()
				}
				
				catch (let e)
				{
					inCont.resume(throwing: e)
				}
			}
		}
	}
1 Like

Looks right to me!

2 Likes