Can I ask if a substring is part of a string using C preprocessor directives?

Viewed 865

Will present the question first, then the reasoning behind it.

Question: Can I ask if a substring is part of a string at compile time? i.e. Can I use preprocessor logic to ask whether "Subst" is part of the string "Text1 Subst Text2..."?

Reasoning: I work on this large C code base, which has a given tracing framework. This framework allows turning on traces based on trace levels - error, warning, info, debug. This is enabled at compile time, meaning that the strings in the debug traces won't even get compiled if trace level doesn't include the debug level. So far nothing new.

Now, in addition, this framework requires modules to specify their trace group as a string. e.g. #define TRACE_GROUP "GRP1". Trace group is displayed in traces and allows filtering traces according to groups - but in run time. This is done using simple C substring functions.

What I'm asking is whether there's a way to do it in compile time. i.e. ask if a trace group is part of a set (like "Grp1 Grp3 Grp7") using preprocessor directives. Reason I need it is the fact that the system is limited in code size. Turning on trace level will fill it with undesirable strings from all modules, while all I want to include is strings from specific modules.

Didn't see any apparent way to accomplish that, maybe I'm wrong.

2 Answers

Can I ask if a substring is part of a string at compile time?

No, you can't.

This framework allows turning on traces based on trace levels - error, warning, info, debug

ask if a trace group is part of a set

So use integer flags separately for that.

#define GROUP1 0x01
#define GROUP2 0x02
#define ENABLED_GROUPS  (GROUP1 | GROUP2)
#define IS_GROUP_ENABLED(x)  ((ENABLED_GROUPS & x) == x)

this framework requires modules to specify their trace group as a string

So use separate macro for enabling the group and separate for specifying the string to be displayed for the "trace group".

Reason I need it is the fact that the system is limited in code size.

Enable compiler optimizations - with string literals and gcc compiler should be able remove calls to strstr or strchr. To be sure, just use integer flags and use preprocessor #if stememnts.

I recently worked with ZephyrOS, take a look at their logging. The prefix that is configured separately from loglevel for modules.


You "could" unroll strstr function and write something that does no external calls and expands to something that is easy to optimize for the compiler:

#define STRSTR_CMP_IN(a, b, i) \
    (a)[i] != (b)[i] ? 0 : !(b)[i + 1] ? 1 :
#define STRSTR_CMP(a, b)  (\
    !(b)[0] ? 1 : \
    STRSTR_CMP_IN(a, b, 0) \
    STRSTR_CMP_IN(a, b, 1) \
    STRSTR_CMP_IN(a, b, 2) \
    STRSTR_CMP_IN(a, b, 3) \
    STRSTR_CMP_IN(a, b, 4) \
    STRSTR_CMP_IN(a, b, 5) \
    STRSTR_CMP_IN(a, b, 6) \
    /* TODO: Add more for longer strings */ \
    0)

#define STRSTR_IN(s, h, i) \
    !(s)[i] ? ((void*)0) : STRSTR_CMP(&(s)[i], (h)) ? &(s)[i] :
#define STRSTR(s, h)  (\
    STRSTR_IN(s, h, 0) \
    STRSTR_IN(s, h, 1) \
    STRSTR_IN(s, h, 2) \
    STRSTR_IN(s, h, 3) \
    STRSTR_IN(s, h, 4) \
    STRSTR_IN(s, h, 5) \
    STRSTR_IN(s, h, 6) \
    /* TODO: Add more for longer strings */ \
    0)

Still:

// ERROR - you can't use subscript operator in the preprocessor.
#if STRSTR("abc", "b")

But it might be easier for your compiler to optimize it out (but ex. gcc compiler optimizes out strstr calls anyway). (You could also move to C++, they have constexpr). Still there might be a good chance that if (STRSTR("Text1 Subst Text2...", "Subst")) will be optimized to just if(1) by your compiler.

It would be easier to define each trace group identifier as a macro that expands to a power of 2 integer constant so that they can be formed into a set, for example:

#define GRPA (1U << 0)
#define GRPB (1U << 1)
#define GRPC (1U << 2)

The module can define the TRACE_GROUP macro to expand to one of those group identifiers, for example:

#define TRACE_GROUP GRPA

The enabled trace groups can be specified in a macro (here, ENABLED_TRACE_GROUPS) set at compile time to expand to the identifiers of the enabled trace groups separated by |, for example:

$ cc -DENABLED_TRACE_GROUPS="GRPA | GRPB" -c foo.c

In an include file (here, "trace.h") that implements the tracing macro, the TRACE_GROUP macro can be compared to the known constants to define a macro (here, TRACE_GROUP_STR) that expands to a string, for example:

trace.h

#ifndef TRACE_H__INCLUDED__
#define TRACE_H__INCLUDED__

/* Trace groups: */
#define GRPA (1U << 0)
#define GRPB (1U << 1)
#define GRPC (1U << 2)

/* define TRACE_GROUP_STR corresponding to TRACE_GROUP as a string */
#ifdef TRACE_GROUP
#if TRACE_GROUP == GRPA
#define TRACE_GROUP_STR "GRPA"
#elif TRACE_GROUP == GRPB
#define TRACE_GROUP_STR "GRPB"
#elif TRACE_GROUP == GRPC
#define TRACE_GROUP_STR "GRPC"
#else
#define TRACE_GROUP_STR "GRPunk"
#endif
#else
#define TRACE_GROUP_STR "GRPundef"
#endif

/*
 * Define the TRACE(...) macro to either call trace(TRACE_GROUP_STR, ...)
 * or do nothing.
 */
#if defined(TRACE_GROUP) && defined(ENABLED_TRACE_GROUPS) && \
    (TRACE_GROUP & ENABLED_TRACE_GROUPS)
#define TRACE(...) trace(TRACE_GROUP_STR, __VA_ARGS__)
#else
#define TRACE(...) ((void)0)
#endif

/* The trace() function. */
void trace(const char *grp, const char *fmt, ...);

#endif

The trace() function declared within "trace.h" can be defined in some utility .c file (here, "trace.c") as required, for example:

trace.c

#include <stdarg.h>
#include <stdio.h>
#include "trace.h"

void trace(const char *grp, const char *fmt, ...)
{
    va_list ap;

    fprintf(stderr, "%s: ", grp);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}

Example usage:

main.c

#define TRACE_GROUP GRPA
#include "trace.h"

int main(void)
{
    TRACE("Hello");
    TRACE("world!");
    return 0;
}

Example build:

$ cc -DENABLED_TRACE_GROUPS="GRPA | GRPB" main.c trace.c -o tracetest

Example run:

$ ./tracetest
GRPA: Hello
GRPA: world!
Related