FYI: devel/poser now available (portable C framework for POSIX services)

zirias@

Developer
Recently, there was a thread about "async sockets". So I thought I'll let you know about:

I just added devel/poser to our tree!

This is a lightweight and portable framework for implementing services in C, using only standard POSIX APIs. It offers an event-based and "modern" API that should be pretty easy to use. It features a main service loop around pselect(), raising events when any file descriptor is ready for reading or writing, a (configurable) threadpool (just create a ThreadJob and schedule it for execution, you will get an event when it finished), generic "daemonizing" code (just give it a location for a pidfile, it handles anything else including correct locking), abstractions for socket connections and servers (supports TCP and local UNIX sockets), and a few utility classes (like a List, a HashTable and so on). TLS support is optional at build time and on by default, and works completely transparent, just enable it when configuring your client or server. The library strictly uses non-blocking I/O and does everything async (and, if not supported by POSIX APIs, simulates async by doing the job on a thread of the threadpool).

API docs can be found here: https://zirias.github.io/poser

In a nutshell, this might be for you if you want to implement an async service in a simple and painless way in C and don't need to scale to more than around 100 concurrent connections, but want to keep it small and simple with little dependencies.

It's definitely not for you if you need to scale to thousands of concurrent connections. In that case, better look for solutions abstracting all the OS-specific solutions (like e.g. kqueue on FreeBSD and epoll on Linux) like e.g. libev.
 
And I'm already preparing a first minor upgrade (adding new features, but not breaking the old API/ABI). It will deliver quite a few minor fixes and reliability improvements, but more importantly, one new feature when built with TLS support:

For a TCP server with TLS enabled, there's now the option to request clients to present a client certificate, optionally validating it against a set of given CAs (their certificates must be available in a single file) and optionally getting a callback for custom validation logic. An example validation callback just logging the certificate meta-data currently available and then accepting any certificate could look like this:

C:
static int validate(void *receiver, const PSC_CertInfo *cert)
{
    (void)receiver;

    PSC_Log_fmt(PSC_L_INFO, "client cert: `%s'",
            PSC_CertInfo_subject(cert));
    PSC_Log_fmt(PSC_L_INFO, "fingerprint (SHA-512): `%s'",
            PSC_CertInfo_fingerprintStr(cert));

    return 1;
}

The new code is already extensively tested using "lab examples". Still, I want to use it in some real project before releasing it, hope to finish these tests in a few days :cool:
 
Since a few days, I'm working on another new feature, and this will take some time as it turned out to be quite complex: Abstractions for configuration.

Every service needs some configuration. Looking at my services, I wasn't happy about how I did it. They all only support configuration by commandline arguments (no config files implemented so far). They all implement their own custom commandline parser, because they all need things that a simple getopt(3) can't handle. So, my idea was to add some "generic" configuration features to poser.

What I roughly planned:
  • A way to describe the structure of the configuration, using "ConfigElement" and "ConfigSection" objects, where a section contains many named elements, and an element can have a bool, string, int or float type (optionally as a list), as well as a "section" type containing another section. (almost completed)
  • A way to add custom parser and validator callbacks for individual elements (to do)
  • Specific parsing and validation error messages for each ConfigElement, also settable by custom parsers/validators (to do)
  • A generic config parser that can stack individual concrete parsers (so you could e.g. have options from command line override options from a config file) (partially done)
  • A concrete parser for command line arguments, supporting POSIX-style short flags (and any way to arrange them), GNU-style long flags as well as positional parameters and one nesting level (sub-section) using a custom format (almost completed)
  • A concrete parser for some to-be-defined config file format (and maybe later more parsers for alternative formats like e.g. JSON or XML) (to do)
  • For command-line: auto-generation of a short "usage" and a detailed "help" message (done)
  • For command-line: ConfigElements that trigger some immediate action, typically used for things like --help or --version (to do)
  • For config-file: auto-generation of a well-commented sample file (to do)
  • An option to enable automatical "paging" of generated output using $PAGER (if set non-empty) if it goes to a terminal and wouldn't fit without scrolling (done)
Well, the amount of code for all that exploded quickly 🙈 Anyways, here's what my latest testing session looked like, including a lot of possible weirdness :cool::
Code:
$ cat parsetest.c
#include <poser/core.h>

int main(int argc, char **argv)
{
    PSC_ConfigElement *e;

    PSC_ConfigSection *root = PSC_ConfigSection_create(0);

    e = PSC_ConfigElement_createString("requiredfile", 0, 1);
    PSC_ConfigElement_argInfo(e, -1, 0);
    PSC_ConfigElement_describe(e, "This file will be frobnicated");
    PSC_ConfigSection_add(root, e);

    e = PSC_ConfigElement_createBool("foo");
    PSC_ConfigElement_argInfo(e, 'f', 0);
    PSC_ConfigElement_describe(e, "Frobnicate with extra foo power");
    PSC_ConfigSection_add(root, e);

    e = PSC_ConfigElement_createFloat("myfloat", .0, 0);
    PSC_ConfigElement_argInfo(e, 'm', "somefloatvalue");
    PSC_ConfigSection_add(root, e);

    e = PSC_ConfigElement_createList(
            PSC_ConfigElement_createInteger("myint", 0, 0), 0);
    PSC_ConfigElement_argInfo(e, 'i', 0);
    PSC_ConfigElement_describe(e, "Sets the frobnication level");
    PSC_ConfigSection_add(root, e);

    PSC_ConfigSection *frobspec = PSC_ConfigSection_create("frobspec");

    e = PSC_ConfigElement_createString("host", 0, 1);
    PSC_ConfigElement_argInfo(e, -1, 0);
    PSC_ConfigSection_add(frobspec, e);
    e = PSC_ConfigElement_createInteger("a", 0, 0);
    PSC_ConfigElement_argInfo(e, 'a', 0);
    PSC_ConfigSection_add(frobspec, e);
    e = PSC_ConfigElement_createList(
            PSC_ConfigElement_createInteger("b", 0, 0), 0);
    PSC_ConfigElement_argInfo(e, 'b', 0);
    PSC_ConfigSection_add(frobspec, e);

    e = PSC_ConfigElement_createSectionList(frobspec, 0);
    PSC_ConfigElement_argInfo(e, -1, 0);
    PSC_ConfigSection_add(root, e);

    PSC_ConfigParser *parser = PSC_ConfigParser_create(root);
    PSC_ConfigParser_addArgs(parser, "parsetest", argc, argv);

    PSC_Config *cfg = 0;

    const char *file;
    if (PSC_ConfigParser_parse(parser, &cfg) >= 0
            && cfg && (file = PSC_Config_get(cfg, "requiredfile")))
    {
        if (PSC_Config_get(cfg, "foo")) printf("foo enabled.\n");
        const double *f = PSC_Config_get(cfg, "myfloat");
        if (f) printf("got float: %f\n", *f);
        const PSC_List *ints = PSC_Config_get(cfg, "myint");
        if (ints)
        {
            printf("got ints:");
            PSC_ListIterator *i = PSC_List_iterator(ints);
            while (PSC_ListIterator_moveNext(i))
            {
                const long *v = PSC_ListIterator_current(i);
                printf(" %ld", *v);
            }
            PSC_ListIterator_destroy(i);
            printf("\n");
        }
        printf("required file: %s\n", file);
        const PSC_List *frobs = PSC_Config_get(cfg, "frobspec");
        if (frobs)
        {
            printf("\nfrobspecs:\n");
            PSC_ListIterator *i = PSC_List_iterator(frobs);
            while (PSC_ListIterator_moveNext(i))
            {
                const PSC_Config *frob = PSC_ListIterator_current(i);
                const char *host = PSC_Config_get(frob, "host");
                printf("\nhost: %s\n", host);
                const long *a = PSC_Config_get(frob, "a");
                if (a) printf("a: %ld\n", *a);
                const PSC_List *bl = PSC_Config_get(frob, "b");
                if (bl)
                {
                    printf("list of b:");
                    PSC_ListIterator *j = PSC_List_iterator(bl);
                    while (PSC_ListIterator_moveNext(j))
                    {
                        const long *b = PSC_ListIterator_current(j);
                        printf(" %ld", *b);
                    }
                    PSC_ListIterator_destroy(j);
                    printf("\n");
                }
            }
            PSC_ListIterator_destroy(i);
        }
        PSC_Config_destroy(cfg);
    }
    else
    {
        PSC_ConfigParser_help(parser, stderr);
    }

    PSC_ConfigParser_destroy(parser);
    PSC_ConfigSection_destroy(root);

    return 0;
}
$ ./parsetest
Usage: ./parsetest [-f] [-i integer [-i ...]] [-m somefloatvalue] requiredfile
        [frobspec [frobspec ...]]

    -f, --[no-]foo
        Frobnicate with extra foo power

    -i integer, --myint=integer
        Sets the frobnication level

    -m somefloatvalue, --myfloat=somefloatvalue
        Set somefloatvalue

    requiredfile
        This file will be frobnicated

    frobspec
        Set frobspec

      Format: host[:k=v[:...]]
      host
          Set host
      k=v: key-value pair, any of the following:
        a=integer
            Set integer
        b=integer {multiple}
            Set integer

    For sub-section arguments delimited by a colon (:), any colons and equals
    signs (=) contained in values must be escaped with a backslash (\).
    Alternatively, values may be quoted in a pair of brackets (between `[' and
    `]') if they don't contain those themselves.
$ ./parsetest -im -i17 42 .7 -i17 --myint=333 -- -foobar --myint "[::1=localhost]:b=22:[a]=17:b=18"
got float: 0.700000
got ints: 17 42 17 333
required file: -foobar

frobspecs:

host: --myint

host: ::1=localhost
a: 17
list of b: 22 18
$

If you're interested, you can peek at my ongoing work in my feature branch (that will be deleted once completed). Beware, the code is kind of ugly 🙈
 
Back
Top