Devember 2019: First ten days
Dec 12, 2019
5 minute read

    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!




    Comments