For more half-baked ideas, see the ideas tag.
Large “coordination” libraries like libvirt unify a lot of disparate features through one API, and as a result they depend on many other libraries and external programs. Libvirt directly links to 20 libraries and requires countless other external programs.
We want users to be able to compile libvirt even when they don’t have the full set of libraries (you don’t need, say, PolKit, libvirt will still work with reduced functionality). The issue though is you end up with code which looks like:
switch (cred[i].type) {
case VIR_CRED_EXTERNAL: {
if (STRNEQ(cred[i].challenge, "PolicyKit"))
return -1;
#if defined(POLKIT_AUTH)
if (virConnectAuthGainPolkit(cred[i].prompt) < 0)
return -1;
#else
/* Ignore & carry on. Although we can't auth
* directly, the user may have authenticated
* themselves already outside context of libvirt
*/
#endif
break;
}
This code is fragile because (a) it’s hard to reason about all the pathways and (b) it’s combinatorially difficult to test all the different permutations of available libraries. This fragility leads to bug reports and possibly worse.
Before I get to the half-baked idea, I’ll throw in another thought: at the moment we do most of this detection at compile time using a long configure script. It might be better to do it at run time. You could imagine how this could work if you were a very patient programmer who liked writing tedious boilerplate:
libaudit = dlopen ("libaudit.so", 0);
//...
if (libaudit) {
int (*audit_add_watch) (struct audit_rule_data **rulep,
const char *path);
audit_add_watch = dlsym (libaudit, "audit_add_watch");
if (audit_add_watch)
r = audit_add_watch (rule, path);
else
goto no_func;
} else {
no_func:
// no libaudit, do something else
}
The half-baked idea is this: Write the code as if all the functions exist. Then transform the code into the runtime/dlsym version above. In the first iteration, for each libvirt API entry point we compute the sum of all optional libraries/functions that are required to execute that entry point, and we generate checks like this:
virFoo ()
{
// The following checks are generated automatically:
if (!libaudit)
return error ("virFoo: you need to install libaudit");
if (!libaudit_audit_add_watch)
return error ("virFoo: wrong version of libaudit, "
"requires audit_add_watch function");
//..
// Here we run the programmer's code:
//..
}
The first iteration is very conservative. In the second iteration of the project we’d allow the programmer to write fallback code, so that partial API functionality is available even if not all the libraries are. But how to do that and avoid the #ifdef problem?
I think you should be allowed to write alternate functions:
authenticate ()
{
return polkit_context_is_caller_authorized (pkcontext, ...);
}
authenticate ()
{
return 1;
}
(Remember this is not C, but some sort of C with transformations applied to it).
Our C transformation chooses the “best” function to call at runtime, where best is simply the one which has the most libraries available. In the above case, the first version of authenticate is chosen if the PolKit library is available, the second version if not.