When I first looked at boost::asio, I didn't quite get it. I looked at the tutorials and thought "That's too easy". Then, I looked at the examples, and tought "That's too hard". Then, as weeks went by, I found more guides and articles about it, and I hit the point where I could bring the porridge analogy home and say "That's just right".
On my pet project, I'm using libssh2. So, I thought "Why not use boost::asio to frame my usage of libbsh2? I could build my libssh2 C++ wrapper, based on asio". Since I'd had done some work on the ssh2_exec example, I chose it as my starting point for integration with asio.
I did do some googling first, to see if anyone had had a shot at anything like this, but found nothing I could use. So, according to Google, I seem to be the first misguid... er, I mean, brave soul to document my attempt at it.
tcp::socketwas easy. And I got asio's portability, to boot. No more mucking about with
WHERE_ARE_MY_SOCKS, and all that jazz.
Then, I took a closer look at libssh2's implementation. It's an API composed largely of what we might call "C's shot at coroutines". Each of these functions has a group of discrete steps, and maintains its state, so that it knows which step is currently executing. Since all this is non-blocking, the execution of a function is a loop of invocations until all its steps are done, at which point the state is reset, so that the cycle may begin anew, when that function is called again.
Regarding the "non-blocking" part - libssh2 implementation is always non-blocking. Its API may be blocking, if the user calls
libssh2_session_set_blocking(LIBSSH2_SESSION *session, int blocking)with
blocking != 0. This only affects the user's code (and only that particular LIBSSH2_SESSION). Once control is handed to the called libssh2 function, it is always non-blocking. libssh2 implements a macro loop, to ensure the behaviour is consistent with the user-requested blocking status - if blocking is on, the loop won't return until all the function's steps have been performed.
While going through their steps, libssh2 functions perform plenty of read/write on the socket. And all this activity will happen without any control being given back to the user's code, even if we're not blocking; in my case, this "user's code" includes asio. Which is also observing the socket. While at first this sounded troublesome, after further research, I'm inclined to believe it may actually be harmless. Let's see...
- App creates instance of SSHWrapper (an originally - and brilliantly, if I do say so myself - named C++ wrapper for libssh2, using asio).
- App invokes
SSHWrapper.Connect(), which will open an ssh connection to a host, somewhere over the thing with the pot of gold.
- SSHWrapper sets a handler on socket read/write (which calls, e.g.,
libssh2_session_handshake()), and starts an io_service event loop.
- Activity on the socket fires our handler.
- The handler calls the libssh2 function.
- If libssh2 work isn't done, "rearm" the event handler before exiting and repeat steps 4-6.
We could have problems between steps 5 and 6; at this point, the libssh2 function could (meaning "most likely will") trigger socket activity outside of asio's control, which could create further read/write events. OTOH, at this point, a) there is no active handler for such activity (we will only have an active handler again on step 6; and b) all this work is happening on the same thread (my next few PoCs will cover multithreading, where I intend to find out exactly what "Asynchronous completion handlers will only be called from threads that are currently calling io_service::run()" means), so there's no risk of reentering the handler, even if it was still defined.
One detail was puzzling me, though. Looking closely at steps 3 and 4, you'll notice that there's activity on the socket before any libssh2 function is called, since the function is called only in step 5, in the handler itself. However, something was firing the events and calling the handlers. Some testing revealed that calling
tcp::socket::async_write_some()with an instance of
boost::asio::null_buffers()fires the event immediately, which does make sense - I wouldn't call a write function if I wasn't ready to write, right (I'm already conceiving some multithreading tests regarding this one, actually)?
So, even though some details are still a bit foggy, and merit further experimentation, I have a working example. This is equivalent to libssh2's ssh2_exec example, and I'll be discussing it here in the following posts.