arsd 11 progress report

Posted 2023-03-20

The work-in-progress new arsd.core module has been formally added to git master. Still a lot of work to do, but some concepts finally becoming code.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

What Adam is working on

I've put some time into arsd.core and some into my websocket implementation. The websocket now offers reason codes in its onclose callback and arsd.core is officially in the repo and starting to accumulate some functionality.

I generated a version of the ddoc written so far here: http://dpldocs.info/experimental-docs/arsd.core.html

Event loop

The core feature of the arsd.core module is a unified event loop. Having one implementation that served the different needs of all the modules is actually non-trivial... but one interface that unifies them is not so bad.

The approach I'm moving forward with is there's four kinds of event loops that can all interoperate:

  • UI loops, based on simpledisplay's implementation. This is a general-purpose loop that should stay responsive at all time and prioritizes ui events.
  • Task runners, based on cgi's implementation. This is an i/o focused loop that can take some time on a task if it needs to, but should generally stay available to respond to new requests. Incoming tasks are distributed among them automatically.
  • Ad-hoc loops, based on a merger of terminal and http2's implementations. This is a general-purpose loop that is expected to be entered and exited randomly while the thread does other work.
  • Helper workers, which is the same implementation as task runners, but with a different purpose - running independent tasks that are not expected to remain reliably responsive.

I also will change simpledisplay's window post event function to use a native window message on the Windows platform, to help solve some troubles with internal modal event loops there stalling the custom event processing.

The Linux implementation of this is almost done and the Windows implementation is in the works (most of it is already in sdpy and cgi, but adapting to the new system is gonna take some work). I read the man pages relevant to the BSD/Mac implementation and expect no real trouble there either.

I spent more time thinking about the thread types than anything else, but after deciding to start moving and seeing that .net has some significant similarities gave me some confidence this plan isn't bad.

Child process communication

Phobos' std.process is solidly ok for some tasks, but really problematic for interleaved streaming communication with the child. This is something that I wanted to fix, and a good test case for the event loop implementation, so I implemented an ExternalProcess class and some stream adapters with it.

The ExternalProcess class by itself is fairly pedestrian, offering callbacks for data incoming on the stdout and stderr from the process, and a function to send something to its stdin. But it did force the issue of signal handlers in the event loop which was a bit of a hassle (I decided to go with the traditional self-pipe trick, except with an eventfd instead of a pipe on Linux, since the signalfd masking affects child processes unless you reset it and since I don't mandate everything goes through me, it might not be reset and just bleh. Setting the signal handler is bad enough but at least those aren't inherited across execs) and it gave a nice test case for the stream adapters.

The stream adapters also proved themselves with it. Inspired by unix pipes, these work by being fed raw bytes data on one end and giving data out in requested formats - like int, string, array, etc. - on the other. It has a kind of flow control: if the consumer requests data that isn't in the buffer, it can wait for more. Inside a fiber, this yields until more data is fed. This is thus an easy way to adapt the callback-based api of ExternalProcess into a linear bit of task code. The "data received" callback need only call stream.feedData(just_received) and the task is triggered again.

One thing still missing at this point is the backpressure back to the external process. If the stream is not being consumed and the buffer is full, it should actually stop reading from the pipe, which will put the other process producing the data on hold, just like how the stream consumer task is on hold when its buffer is empty. My current implementation assumes you want to read all the data; by the time you get to your callback, it has already been read off the source pipe and the request for more has been issued. I'll have to adjust the design somewhat before release to correct this. Most likely I'll make a method to explicitly request more data that you call at the end of your callback, but it also needs to be callable elsewhere (e.g. by the stream when it finds its buffer is running low) so it can't just be passed to it.

It isn't quite done, but it is almost there.

Exception framework

Back in 2015, I wrote a module called arsd.exception to describe some ideas I had to make exceptions easier to use. The module worked, but the benefit it brought didn't justify the build system trouble that importing it would bring. With arsd.core, the calculation has changed, providing an opportunity to actually use those concepts.

In the time since I wrote the original module, I revisited the concept in a subsequent blog post and refined it somewhat with more experience trying to work with other people's exception ideas, seeing how some of it wasn't so great in practice.

The result is I now have a new system that I'll actually use, and indeed, it is already starting to provide some benefit - the first commit of arsd.core actually focused on this, to improve some exception messages that were substandard.

The key pieces right now are a base class that tries to undo some antipatterns in D's base normal Exception, then a couple child classes for platform-specific errors (imported from simpledisplay), and a template to generate new exception classes out of arguments, to provide an alternative to throw new Exception that exceeds it in both convenience and functionality.

Along with these, I wrote an overview of the concept in the documentation, too. The key goal is to forward structured data up while still being convenient to use, to satisfy programmer laziness. After starting to use it, I think it is a partial success so far. I'll continue to refine between now and the release.

One thing I'm missing in particular is some kind of multiple inheritance. For example, a File Not Found error might simultaneously be both a FileNotFoundException and a WindowsApiException or an ErrnoException depending on the called function. I think I will try exposing this as a structured tags member on the exception base, but I'm not sure yet.

But even if I can't, just having the basic info attached is better than plain Exception and random strings.

populateFromUda reflection helper

arsd.core also includes some general purpose helper functions, ranging from intToString (which is primarily there to help with the exception framework) to some higher level reflection helpers. One of them is populateFromUda, which populates a struct from a list of attributes. This is similar to the "blueprint" pattern Steven Schveighoffer talked about in dconf last time, and something I've already used to some extent inside cgi.d and minigui.d.

Steve's talk: Slides PDF | Main Video | Q&A Video

The benefit of using this is both providing an anchor to document the possible udas as well as improving compile times. Moreover, the struct could also be populated from runtime data and potentially used to load things off files too, e.g. an interface designer's output for a gui. Putting it in the core will make it easier to interoperate too, well the reflection helper itself only of limited value sharing, but the actual types being shared can help - cgi and minigui, for example, might reuse some form members for web or desktop presentations.

arsd.core has a strict no-phobos policy. In fact, the only modules it is allowed to import at all are object and the core.* namespaces. This is because it is a foundational library to my other modules, many of which were previously stand alone. Anything I add here is going to have ripple effects downstream. Since phobos imports are often quite expensive in compile times and sometimes unstable across compiler versions, these effects can be significant in some cases. (And in other cases, they can quickly disappear in the noise. Worth remembering that the cost of a Phobos import is amortized across the whole of the application, so as an application grows and finds value in the lib anyway, you've already paid for it so reusing it has small marginal cost. arsd.core is a bit of a special case given its foundational position to a wide variety of other libraries and applications.)

But not using Phobos is also an opportunity to examine what it does right - which will prove to be the things we really want to use and will miss it when it is banned - and what it does wrong, by looking at new ways to achieve goals despite the limitation.

Event-driven applications is something Phobos has missed. Message passing is something it offers, but doesn't realize full potential. Reflection is something it does a lot of actually useful things toward, but at a cost. Phobos tries to provide building blocks you can compose in a functional style for reflection. Trying to do this better has led to a lot of ideas, none of which have worked out especially well compared to just using the language features directly. Going directly to a higher level "blueprint" (and putting a name to it I do think will help a lot!) has some potential and having a couple functions to point to rather than just some scattered internal implementations might help prove its validity.

Conclusions

Very little of the things described here or the remainder of the plan for arsd.core is actually new things, but that's part of the point: it is supposed to be a consolidation and unification of existing patterns to enable more interoperability. I think this is going to deliver more than enough value to justify the build system breakage and occasional annoyance before long - indeed, some of the benefits are already materializing in the newest commits in git.

I'm still aiming for a release date in May. We'll see how things keep going in the next couple months.