Feature flags / toggles when artifact is a library and flags affect C or C++ headers
Asked Answered
A

6

6

There exists quite a bit of discussions on feature flags/toggles and why you would use them but most of the discussion on implementing them center around (web or client) apps. If your product/artifact is a C or C++ library and your public headers are affected by the flags, how would you implement them?

The "naive" way of doing it doesn't really work:

/// Does something
/**
 * Does something really cool
#ifdef FEATURE_FOO
 * @param fooParam describe param for foo
#endif
 */
void doSomethingCool(
#ifdef FEATURE_FOO
    int fooParam = 42
#endif
);

You wouldn't want to ship something like this.

  • Your library that you ship was built for a certain feature flag combination, clients shouldn't need to #define the same feature flags to make things work
  • The ifdefs in your public header are ugly
  • And most importantly, if you disable your flag, you don't want clients to see anything about the disabled features - maybe it is something upcoming and you don't want to show your stuff until it is ready

Running the preprocessor on the file to get the header for distribution doesn't really work because that would not only act on feature flags but also do everything else the preprocessor does.

What would be a technical solution to this that doesn't have these flaws?

Adelaadelaida answered 2/8, 2018 at 15:37 Comment(6)
"If you disable your flag, you don't want clients to see anything about it - maybe it is something upcoming and you don't want to show your stuff until it is ready" This shouldn't really be an issue: Simply don't merge unfinished features from development branch to release branch with your version control.Bacchae
That is the "long lived feature branch" approach, which some people prefer, others propose merging early and disabling functionality that isn't ready for prime time with feature toggles - these discussions are referred in the linked questions.Adelaadelaida
Aside: From a C perspective, an explicitvoid should exist in the declaration parameter list: void doSomethingCool(#ifdef FEATURE_FOO int fooParam = 42 #else void #endif );. This complicates OP's posted style for C.Hirundine
In C there's no default args so that would be a different beast entirely.Adelaadelaida
I really don't get your question. Your function might or might not have a parameter, depending on the phase of Moon? How can you expect anyone to be able to use it?Athalie
Not the phase of a moon, a build setting in the system that produces the library. And clients would either target the "on" or the "off" setting (but not both). So you'd be able to spin both a "beta" and a "stable" build off the same branch.Adelaadelaida
M
2

This kind of goo ends up in a codebase due to versioning. Broad topic with very few happy answers. But you certainly want to avoid making it more difficult then it needs to be. Focus on the kind of compatibility you want to provide.

The syntax proposed in the snippet is only required when you need binary compatibility. It keeps the library compatible with a doSomethingCool() call in the client code (passing no argument) without having to compile that client code. In other words, the client programmer does nothing at all beyond copying the updated .dll or .so file, does not need any updated headers and it is entirely your burden to get the feature flags right. Binary compatibility is pretty difficult to pull off reliably, beyond the flag wrangling, easy to make a mistake.

But what you are actually talking about is source compatibility, you do provide the user with an updated header and he rebuilds his code to use the library update. In which case you don't need the feature flag, the C++ compiler by itself ensures that an argument is passed, it will be 42. No flag required at all, either on your end or the user's end.

Another way to do it is by providing an overload. In other words, both a doSomethingCool() and a doSomethingCool(int) function. The client programmer keeps using the original overload until he's ready to move ahead. You also favor an overload when the function body has to change too much. If these functions are not virtual then it even provides link compatibility, could be useful in some select case. No feature flags required.

Mahoney answered 2/8, 2018 at 15:38 Comment(2)
It's not only about compatibility - that part is pretty well understood. The thing that poses the most problems is the third bullet point - if you don't want unfinished things turning up in your headers, neither #ifdef or overloads/defaults really work because both of those are visible in your headers.Adelaadelaida
I tried to explain that the user never needs to see that flag. Either because he never needs to see the header file update (binary compatibility) or never needs the flag at all (source compatibility). Well, gave it shot.Mahoney
A
2

I'd say it's a relatively broad question, but I'll trow in my two cents.

First, you really want to separate the public headers from implementation (source and internal headers, if any). The public header that gets installed (e.g., at /usr/include) should contain function declaration and, preferably, a constant boolean to inform the client whether the library has a certain feature compiled in or not, as so:

#define FEATURE_FOO 1
void doSomethingCool();

Such a header is generally generated. Autotools is de facto standard tools for this purpose in GNU/Linux. Otherwise you can write your own scripts to do this.

For completeness, in .c file you should have the

void doSomethingCool(
#ifdef FEATURE_FOO
    int fooParam = 42
#endif
);

It's also up to your distribution tools to keep the installed headers and library binaries in sync.

Amersham answered 7/8, 2018 at 7:20 Comment(0)
S
0

Use the forward declarations

Hide implementation by using a pointer (Pimpl idiom)

this code id quoted from the previous link:

// Foo.hpp
class Foo {
public:

    //...

private:
    struct Impl;
    Impl* _impl;
};

// Foo.cpp
struct Foo::Impl {
    // stuff
};
Sheikh answered 7/8, 2018 at 12:5 Comment(0)
P
0

Binary compatibility is not a forte of C++, it probably isn’t worth considering. For C, you might construct something like an interface class, so that your first touch with the library is something like:

struct kv {
     char *tag;
     int   val;
};
int Bind(struct kv *compat, void **funcs, void **stamp);

and your access to the library is now:

#define MyStrcpy(src, dest)  (funcs->mystrcpy((stamp)(src),(dest)))

The contract is that Bind provides/constructs an appropriate (func, stamp) pair for the attribute set you provided; or fails if it cannot. Note that Bind is the only bit that has to know about multiple layouts of *funcs,*stamp; so it can transparently provide robust interface for this reduced version of the problem.

If you wanted to get really fancy, you might be able to achieve the same by re-writing the PLT that the dlopen/dlsym prepare for you, but:

  1. You are grossly expanding your attack surface.
  2. You are adding a lot of complexity for very little gain.
  3. You are adding platform / architecture specific code where none is warranted.

A few downsides remain. You have to invoke Bind before any part of your program/library attempts to use it. Attempts to solve that lead straight to hell (Finding C++ static initialization order problems), which must make N.Wirth smile. If you get too clever with your Bind(), you will wish you hadn’t. You might want to be careful about re-entrency, since a given client might Bind multiple times for different attribute sets (users are such a pain).

Petaloid answered 7/8, 2018 at 14:48 Comment(0)
A
0

That's how I would manage this in pure C.

First of all the features, I would pack them in a single unsigned int 32/64 bits long to keep them as compact as possible.

Second step a private header to use only in library compilation, where I would define a macro to create the API function wrapper, and the internal function:

#define CoolFeature1 0x00000001    //code value as 0 to disable feature
#define CoolFeature2 0x00000010
#define CoolFeature3 0x00000100
.... // Other features

#define Cool CoolFeature1 | CoolFeature2 | CoolFeature3 | ... | CoolFeature_n

#define ImplementApi(ret, fname, ...)    ret fname(__VA_ARGS__)  \
                                         { return Internal_#fname(Cool, __VA_ARGS__);}  \
                                         ret Internal_#fname(unsigned long Cool, __VA_ARGS__)
#include "user_header.h"    //Include the standard user header where there is no reference to Cool features

Now we have a wrapper with a standard prototype that will be available in the user definition header, and an internal version which keep an addition flag group to specify optional features.

When coding using the macro you can write:

ImplementApi(int, MyCoolFunction, int param1, float param2, ...)
{
    // Your code goes here
    if (Cool & CoolFeature2)
    {
        // Do something cool
    }
    else
    {
        // Flat life ...
    }
    ...
    return 0;
}

In the case above you'll get 2 definitions:

int Internal_MyCoolFunction(unsigned long Cool, int param1, float param2, ...);
int MyCoolFunction(int param1, float param2, ...)

You can eventually add in the macro, for the API function, the attributes for export if you're distribuiting a dynamic library.

You can even use the same definition header if the definition of ImplementApi macro is done on the compiler command line, in that case the following simple definition in the header will do:

#define ImplementApi(ret, fname, ...)    ret fname(__VA_ARGS__);

The last will generate only the exported API prototypes.

This suggestion, of course, is not exhaustive. There a lot of more adjustments you can do to make more elegant and automatic the definitions. I.e. including a sub header with function list to create only API function prototypes for the user, and both, internal and API, for developers.

Account answered 10/8, 2018 at 9:25 Comment(0)
I
0

Why are you using defines for feature flags? Feature flags are supposed to enable you to turn features on and off runtime, not compile time.

In the code you would then case out implementation as early as possible using interfaces and concrete classes that are chosen based on the feature flag.

If users of the header files arent supposed to be able to access the feature flags, then create header files that you dont distribute, that are only included in the implementation c/cpp files. You can then flip the flags in the private headers when you compile the library that they link to.

If you are keeping features internal until you are ready to release, you can move the feature flag into the public header, or just remove the feature flag entirely and switch to using the new implementation.

Sloppy example if you want this compile time:

public_class.h

class Thing { public: void DoSomething(); }

private_class_feature1.h #define USE_FEATURE_1

class NewFeatureImp { public: static void CoolNewWay1(); }

public_class.cpp #include “public_class.h” #include “private_class_feature1.h”

void Thing::DoSomething() { #ifdef USE_FEATURE_1 NewFeatureImpl::CoolNewWay(); #else // Regular impl #endif }

Iffy answered 15/9, 2022 at 2:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.