[asio-users] New Lua bindings for ASIO
Brought to you by:
chris_kohlhoff
From: Vinícius d. S. O. <vin...@gm...> - 2023-04-03 13:52:58
|
Hi list, I'd like to announce a project I've been working on for the past few years, and I think it finally reached a point where it may be generally useful for more people. Emilua brings Lua bindings for ASIO. Here are a few links for the project: - Repository: https://gitlab.com/emilua/emilua - Documentation: https://docs.emilua.org/api/0.4/ - Blog: https://blog.emilua.org/ - Homepage: https://emilua.org/ Initially I wanted a playground to experiment with async & concurrent IO for assorted reasons. Upon evaluating popular programming languages I came to the conclusion that Ruby and Lua would be good enough for the things I had in mind, and I decided to go with Lua. Some time later Ruby independently got better support for concurrent IO. Another similar project exists, but it's similar only in its dependencies (Boost.Asio), and nowhere near the capabilities I managed to achieve with Emilua: https://github.com/ignacio/LuaNode As a hint for what are such capabilities, newer Python releases failed to actually make good use of their subinterpreters: https://lwn.net/Articles/820424/ Python subinterpreters is an interesting idea, but few projects explore this kind of idea as far as I'm aware. Meanwhile Emilua not only allows you to spawn multiple Lua VMs to exploit Boost.Asio thread pools in a similar fashion to what Christopher Kohlhoff demoed with executors[1], but also allows you to spawn new Lua VMs isolated into Linux namespaces for sandboxing purposes (for the next versions I plan to support Landlock and Capsicum as well): https://docs.emilua.org/api/0.4/tutorial/linux_namespaces.html As for the implementation, a realization simplified the interface for Lua programs a lot. Much of Boost.Asio's complexity lies in emulating fibers on top of poor (but high-performance) language constructs. You create asynchronous agents to basically perform a sequential composition of asynchronous operations (as in... calling another function when the previous "returns"). Now, this could also be said to be poor promises/futures, but asynchronous agents also have associated characteristics (such as executors, allocators, and cancellation slots) that are closer to the fiber world. Some of these associated characteristics would be variables in the fiber stack, but in ASIO a convoluted vocabulary must be used to pass them around. Other associated characteristics would be implicit associated context to each fiber (e.g. cancellation slots). Therefore, for Emilua, I rely exclusively on (Lua) fibers to represent asynchronous agents. When you start a new asynchronous operation, the operation will preserve associated characteristics such as executors. Interrupting a fiber[2] causes the invocation of the cancellation slot's handler (that is transparently installed when a fiber initiates an asynchronous operation). In short, intent is captured out of the usage around fiber vocabulary, and that is translated to Boost.Asio patterns (e.g. remap-post-to-defer, work guards as required, ...). The interface for Lua programs is easy enough that you don't need an expert to make programs in it. The point is that you're using Lua to prototype things that don't require high-performance. Once the algorithm is in place, translation to Boost.Asio would be trivial (just add all the boilerplate code to implement protocols such as completion tokens, proper executors usage, etc). My stand is that the translation would be trivial because it's just boilerplate (mechanical work), and the Lua program will still use all the Boost.Asio concepts (you have sockets, acceptors, the interfaces are mostly the same...). And if you do not need performance for some module in your program, you could write only that part in Lua (the project license is very permissive, and in the future I intend to make embedding it in your own C++ project even easier). For the code that you do write in Lua, the runtime should never crash, and I've done my best to have as few bugs as possible. Boost.Asio flexible threading layout is retained in Emilua. The runtime starts with 1 asio::io_context / 1 thread. Calling spawn_context_threads() spawns more threads to the asio::io_context that manages the calling Lua VM. You may call spawn_vm() to spawn more Lua VMs. Each Lua VM is managed by its own strand. You can spawn more VMs to the same asio::io_context as the caller (the default), or you may spawn it in a new asio::io_context. You may even pass concurrency hints when spawning VMs in new asio::io_contexts. You may also configure threading layout when building the runtime so different algorithms (e.g. implicit strands) and defines (e.g. BOOST_ASIO_DISABLE_THREADS and BOOST_ASIO_CONCURRENCY_HINT_UNSAFE) are used. So, yeah, pretty much every threading layout made possible by Boost.Asio is also possible here. Furthermore, the API for communication among Lua VMs is influenced by the actor model. For a start, that means you may include the address of another actor in the message itself to allow arbitrary messaging topology. You can even spawn actors isolated through Linux namespaces to have a protection model similar to what FreeBSD's Capsicum project offers. The exception type used to communicate errors to Lua programs is basically a wrapper around std::error_code. You may even create your own error categories from Lua programs (the single-category-instance will be achieved by integrating Lua error categories with Lua modules as modules are also unique and global objects even when multiple Lua VMs exist). Portable error comparison through std::generic_category() is also available. You may even have localized error messages for your own categories. Emilua doesn't offer scatter-gather operations for Lua programs. If you do need scatter-gather operations, write a C++ plugin. Yes, there is support for native plugins through Boost.DLL. I even created a plugin to integrate Boost.Asio execution contexts with GTK+'s GLib event loops: https://gitlab.com/emilua/glib So it's possible to create Lua bindings for your own Asio-based libraries as well. For instance, bindings for Boost.Redis could be created as a separate plugin and used transparently by Lua programs. That is an opportunity for authors of Asio-based libraries to widen their audience. There's a pretty huge document detailing the internals of the project (more than what you want to know): https://docs.emilua.org/api/0.4/tutorial/internals.html Speaking of integration, as a side effect of this project, I end up reporting and fixing bugs in many open source projects[3][4][5][6][7] (including Boost.Asio itself). For the buffer type involved in IO operations, Emilua was heavily influenced by Golang's slices[8]: https://docs.emilua.org/api/0.4/ref/byte_span.html As in Boost.Asio, the proactor model is followed. A fiber blocks upon starting an asynchronous operation, and it'll stay blocked until the associated completion event is generated. Buffers involved in the operation need to stay alive until the completion event is received, but that's already guaranteed by the use of buffers inspired by Golang's slices. Many generic algorithms available in Boost.Asio are also available for Emilua, such as ip.connect(), stream.write_all(), stream.read_at_least(), and others. However, the use of fibers makes programming so convenient that one could just as easily implement them by hand if one really wants to. Fibers also give you vocabulary to deal with Boost.Asio's composed operations problems[9]: https://docs.emilua.org/api/0.4/tutorial/streams.html#composed-operations Bindings for APIs only available for certain operating systems also exist. For instance, on UNIX systems you may make use of setresuid(). On Windows, you may make use of TransmitFile(). Furthermore, optimizations specific for certain operating systems are also available even when Boost.Asio itself doesn't offer them yet (e.g. GetAddrInfoExW() will be used if you compile the project with _WIN32_WINNT >= 0x0602). Meson wrap system is used to make builds on Windows easier without impacting Linux builds[10]. Unfortunately most CI systems are disappointingly greedy w.r.t. Gitlab account permissions, so I don't enable them in the main repo and there will be no "passing/failing" badges that are so common nowadays. As for features, there are plenty. Here's a short list with extra stuff that I didn't mention above: - Usual Asio stuff: TCP, UDP, TLS, address/service forward/reverse name resolution, UNIX sockets, pipes, files, UNIX signals, clocks & timers, serial ports. - Extensions on top of Asio customization mechanisms (e.g. SCM_RIGHTS, TransmitFile(), SNI on TLS, Boost.Asio workarounds). - Stuff unrelated to Asio: filesystem, basic ctty/pty support & job control, JSON, HTTP, WebSocket, basic regex support. - A generic scanner to parse textual streams inspired by AWK. It's much more powerful than the asio::read_until() you're used to, but it still retains composability powers. - Documentation is not only available online, but it's also installable as manpages. The plan for the next release is to improve the module system and create a package manager. I experimented with guix in the past, but I've grown dissatisfied with it over time. [1] https://github.com/chriskohlhoff/executors/blob/master/src/examples/executor/actor.cpp [2] https://docs.emilua.org/api/0.4/tutorial/interruption.html [3] https://gitlab.gnome.org/GNOME/glib/-/merge_requests/1960 [4] https://github.com/shadow-maint/shadow/issues/635 [5] https://reviews.llvm.org/D105758 [6] https://github.com/skvadrik/re2c/issues/343 [7] https://lists.isocpp.org/std-proposals/2021/07/2809.php [8] https://go.dev/blog/slices-intro [9] https://sourceforge.net/p/asio/mailman/asio-users/thread/5357B16C.6070508%40mail1.stofanet.dk/ [10] https://brennan.io/2020/05/08/meson/ -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/ |