After 10 long days, I’m ready to share my new journey with Dlang.
Build system
D documentation was really interesting and the language seemed promising. But I needed to start somewhere and I’ve chosen to start by creating the boilerplate files and building them; after all I want to create a shared library (libtt) and an executable linked to it (tt). This is straightforward in C/C++ but I have no idea about D.
Looking at the build systems with D support, there are dub1, dlang package manager shipped alongside the compiler, and meson2, which has seen a huge increase in adoption over the last year, despite not having reached its 1.0 release.
I have chosen meson, because I am already familiar and support my use case really well. The problem was that D support for meson is really lacking and the language itself has rarely used at a system level. I had to discover and use some “hacks” to properly build and obtain a working executable. And this took most of my time during these first days.
I will work more during the next weeks to improve both meson and the D compilers, but it will require a lot of time.
Parsing
Back at the fun part of this Devember; one of the first thing to develop is the environment file and service parser.
For the environment file, that allows the service to be configurable, and the service files themself, I need a parsing library. After some search, I’ve come across on a new one called libconfini3. It is really minimal and well documented. Now I just have to call its method from D.
“Interfacing with C”4 documentation refers to the deimos project: d sources generated from C headers, which allow D programs to use C libraries. Creating a wrapper is straightforward thanks to dstep5, and the result works out of the box flawlessy. You can see d sources to use libconfini in libconfini-d6.
Note: There is another way to use C libraries from D which is dpp[^10], a wrapper over the dmd compiler.
An example environment file is:
VERBOSITY=1
STAGE2=/etc/tt/stage2
libconfini parsing works by defining a IniFormat
, describing what is allowed and
what not, the characters used for delimiting keys from values, and other
configuration flags. Here I have created a function to get the IniFormat
:
import confini;
IniFormat get_env_format() {
IniFormat env_format;
env_format.delimiter_symbol = IniDelimiters.INI_EQUALS;
env_format.case_sensitive = 1;
env_format.semicolon_marker = IniCommentMarker.INI_IGNORE;
env_format.hash_marker = IniCommentMarker.INI_IGNORE;
env_format.multiline_nodes = IniMultiline.INI_NO_MULTILINE;
env_format.section_paths = IniSectionPaths.INI_NO_SECTIONS;
env_format.no_single_quotes = 1;
env_format.no_double_quotes = 0;
env_format.no_spaces_in_names = 1;
env_format.implicit_is_not_empty = 1;
return env_format;
}
Note: To discover more about the IniFormat and libconfini, browse its online docs7 or its man pages.
To parse a file, the function load_ini_path
is used:
int load_ini_path (
const char * path,
IniFormat format,
IniStatsHandler f_init,
IniDispHandler f_foreach,
void * user_data
)
where path
is the path of the file, format
contains the parsing rules,
f_foreach
is a callback function that actually process the parsed key/value
pairs and user_data
is some data that is kept over the parsing.
f_init
won’t be used so let’s skip it for now.
The problem here is that we have to pass as f_foreach
a D function as a C callback, so we have to match the signature (in
D) extern (C) const int function(IniDispatch *, void*)
.
I’ve tried using a closure and adding the tag extern (C)
, but despite my
efforts, it doesn’t work. An idea appears in my mind: what if I add an alias to
this signature?
extern (C) alias IniCallback =
const int function(IniDispatch* dispatch, void* v_null);
It acually compiles. Now let’s add a function that return this exact signature:
extern (C) IniCallback get_env_parse_callback() {
return (IniDispatch* dispatch, void* env_p) {
return 0;
};
}
And then call load_ini_path
from our testing main:
import std.stdio : writeln ;
import std.string : toStringz ;
int main() {
if (load_ini_path(
toStringz("/path/to/my/env"),
get_env_format(),
null,
get_env_parse_callback(),
null)) {
writeln("Something went wrong");
return 1;
}
return 0;
}
And it works! We have been able to use the parsing library libconfini from D code. And withouth too much of works, the result is pritty much readable.
Note: We use the method char* toStringz(string)
to convert a D string
into a C string.
This parsed line that we have in f_foreach
should be processed and saved
somewhere so that we can later load its values into a service environment.
The suited data structure is an associative array which is also really simple to
use in D:
string[string] myass;
Thouth this array can’t be directly passed to void* user_data
(I’ve done way many tests, and the cast from void*
to string[string]
always fail). So the
easiest work around is to to create a struct containing the associative array:
struct EnvWrapper {
string[string] hash;
}
Now our main
become like this:
int main() {
EnvWrapper env;
if (load_ini_path(
toStringz(path),
get_env_format(),
null,
get_env_parse_callback(),
&env)) {
writeln("Something went wrong");
return 1;
}
return 0;
}
And our callback:
extern (C) IniCallback get_env_parse_callback() {
return (IniDispatch* dispatch, void* env_p) {
ini_unquote(dispatch.data, dispatch.format);
ini_string_parse(dispatch.value, dispatch.format);
auto env = cast(EnvWrapper*)env_p;
auto key = to!string(dispatch.data);
auto val = to!string(dispatch.value);
env.hash[key] = val;
return 0;
};
}
The functions ini_unquote
and ini_string_parse
are used to sanitize (i.e.
removing escaping chars) and removing the quotes according to the format.
This time the conversion is the other way, from C strings to D strings using
to!string
.
Conclusion
I have much fun writing this piece of code, way less fun writing the files for the build system. I hope that this can be improved because it’s really a shame to have such unexpressed potential.
On the next days I’ll write the good for the service parser too, and actually experiment with the service dependencies checker, which I think will be one of the critical parts.
Stay tuned!