1

I have a main.c file containing one or more preprocessor macros defined:

#include <stdio.h>

#define VALUE 12

int main(void) {
    printf("This file is in version %s and contains value %d\n", VERSION, VALUE);
    return 0;
}

I want to export a main2.c file with only the #define VERSION "1.0" applied to the original source file.

What I tried:

  • gcc -DVERSION=\"1.0\" -E will apply ALL the preprocessor directives instead of the single one I want
  • sed 's/VERSION/\"1.0\"/g' will probably replace more than needed, and will need more work if I need more than a single directive
  • cppp is a nice tool but may alter the source file a lot. Only supports simple defines with numerical values

Is there any way to execute only parts of preprocessor directives with gcc ?

badbouille
  • 46
  • 5
  • 2
    easiest way would probably be `echo '#define VERSION "1.0"' > main2.c; cat main.c >> main2.c` which just adds `#define VERSION "1.0"` at the start – user253751 Jul 12 '22 at 13:59
  • 1
    The `sed` method looks the way I would go (if I had restrictions that are actually requiring it). If you afraid that `VERSION` could appear more than once in the program, you could define it in a separate header file and process only it, then include into your `main`. – Eugene Sh. Jul 12 '22 at 14:04
  • Why is it "easier" to modify some make file than to modify an actual source file? Why can't you just place the #define in a file of its own? – Lundin Jul 12 '22 at 14:04
  • You're right; but I was looking for something to really 'hide' the original variable. Truth is this is not really a version number but a function name and I want the final .c file to look good without a very long MACRO_NAME everywhere :) – badbouille Jul 12 '22 at 14:05
  • 4
    Then `sed` or similar is your only friend. Again, if you afraid of having ambiguities, you can replace the "very long MACRO_NAME" with something line `VERY@@LONG@@MACRO@@NAME` - which is not legal in C code, but since you are replacing it to generate the actual source you should not care. – Eugene Sh. Jul 12 '22 at 14:10
  • Also I'm currently creating many files based on this single file, using lots of different `#define VERSION xxx`. I'm just looking for an intermediate step to be able to give a clean and processed .c file to every developer if that makes sense. – badbouille Jul 12 '22 at 14:13
  • I was worried `sed` will not do the exact same work as a `#define` (I'll need to convert on the fly a `#define` list), but with long and carefully chosen `MACRO_NAMES` I will make it work. Thanks – badbouille Jul 12 '22 at 14:17
  • I’m voting to close this question because it XY problem. – 0___________ Jul 12 '22 at 14:20
  • 1
    Partial preprocessing is a nifty idea and exactly what you are looking for, by `cppp` only handles `#ifdef` and `#ifndef` lines, it does not perform macro substitutions. I shall investigate if better tools exist or can be produced quickly... – chqrlie Jul 12 '22 at 14:28
  • Thanks a lot @chqrlie, "Partial preprocessing" is the keyword describing my problem. Got me to find the tools `unifdef` and `coan`. Same as `cppp` they only get rid of #ifdef and do not replace the defines – badbouille Jul 12 '22 at 15:28
  • `you can replace the "very long MACRO_NAME" with something line VERY@@LONG@@MACRO@@NAME` Like for example with `{{VERSION}}` and then run your code with jinja2. Or with `` and then run your code via php. You will also get a lot of preprocessing power as a side effect. – KamilCuk Jul 12 '22 at 16:08

3 Answers3

1

Partial preprocessing is a nifty idea and exactly what you are looking for. The cppp utility by Brian Raiter only handles #ifdef and #ifndef lines, it does not perform macro substitution as you require.

Here is a utility I just wrote for this purpose: you can define any number of identifiers on the command line with -Didentifier (expands to 1) or -Didentifier= (expands to nothing), -Didentifier=str or simply identifier=str.

It will substitute identifiers only, preserving comments and strings, but some corner cases are not handled, albeit should not be a problem:

  • no support for non ASCII identifiers.
  • stdio in #include <stdio.h> will be seen as an identifier that can be substituted.
  • some numbers will be parsed as 3 tokens: 1.0E+1.
  • identifiers will not be substituted if they are split on multiple lines with escaped newlines
  • defining include, ifdef and other preprocessing directives will cause them to be substituted, unlike the C preprocessor
  • macro argument names may be substituted whereas the C preprocessor would preserve them.

pcpp.c:

/* Partial preprocessing by chqrlie */

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct define_t {
    struct define_t *next;
    size_t len;
    const char *tok;
    const char *def;
} define_t;

static void *xmalloc(size_t size) {
    void *p = malloc(size);
    if (!p) {
        fprintf(stderr, "pcpp: cannot allocate memory\n");
        exit(1);
    }
    return p;
}

static void add_define(define_t **defsp, const char *str) {
    define_t *dp = xmalloc(sizeof(*dp));
    size_t len = strcspn(str, "=");
    const char *def = str[len] ? str + len + 1 : "1";
    dp->len = len;
    dp->tok = str;
    dp->def = def;
    dp->next = *defsp;
    *defsp = dp;
}

struct context {
    FILE *fp;
    int lineno;
    size_t size, pos;
    char *buf;
};

static int append_char(struct context *ctx, int ch) {
    if (ctx->pos == ctx->size) {
        size_t new_size = ctx->size + ctx->size / 2 + 32;
        char *new_buf = xmalloc(new_size);
        memcpy(new_buf, ctx->buf, ctx->size);
        free(ctx->buf);
        ctx->buf = new_buf;
        ctx->size = new_size;
    }
    ctx->buf[ctx->pos++] = (char)ch;
    return ch;
}

static void flush_context(struct context *ctx, FILE *ft) {
    if (ctx->pos) {
        fwrite(ctx->buf, ctx->pos, 1, ft);
        ctx->pos = 0;
    }
}

/* read the next byte from the C source file, handing escaped newlines */
static int getcpp(struct context *ctx) {
    int ch;
    while ((ch = getc(ctx->fp)) == '\\') {
        append_char(ctx, ch);
        if ((ch = getc(ctx->fp)) != '\n') {
            ungetc(ch, ctx->fp);
            return '\\';
        }
        append_char(ctx, ch);
        ctx->lineno += 1;
    }
    if (ch != EOF)
        append_char(ctx, ch);
    if (ch == '\n')
        ctx->lineno += 1;
    return ch;
}

static void ungetcpp(struct context *ctx, int ch) {
    if (ch != EOF && ctx->pos > 0) {
        ungetc(ch, ctx->fp);
        ctx->pos--;
    }
}

static int preprocess(const char *filename, FILE *fp, const char *outname, define_t *defs) {
    FILE *ft = stdout;
    int ch;
    struct context ctx[1] = {{ fp, 1, 0, 0, NULL }};
    if (outname) {
        if ((ft = fopen(outname, "w")) == NULL) {
            fprintf(stderr, "pcpp: cannot open output file %s: %s\n",
                    outname, strerror(errno));
            return 1;
        }
    }
    while ((ch = getcpp(ctx)) != EOF) {
        int startline = ctx->lineno;
        if (ch == '/') {
            if ((ch = getcpp(ctx)) == '/') {
                /* single-line comment */
                while ((ch = getcpp(ctx)) != EOF && ch != '\n')
                    continue;
                if (ch == EOF) {
                    fprintf(stderr, "%s:%d: unterminated single line comment\n",
                            filename, startline);
                    //break;
                }
                //putc('\n', ft);  /* replace comment with newline */
                flush_context(ctx, ft);
                continue;
            }
            if (ch == '*') {
                /* multi-line comment */
                int lastc = 0;
                while ((ch = getcpp(ctx)) != EOF) {
                    if (ch == '/' && lastc == '*') {
                        break;
                    }
                    lastc = ch;
                }
                if (ch == EOF) {
                    fprintf(stderr, "%s:%d: unterminated comment\n",
                            filename, startline);
                    //break;
                }
                //putc(' ', ft);  /* replace comment with single space */
                flush_context(ctx, ft);
                continue;
            }
            if (ch != '=') {
                ungetcpp(ctx, ch);
            }
            flush_context(ctx, ft);
            continue;
        }
        if (ch == '\'' || ch == '"') {
            int sep = ch;
            const char *const_type = (ch == '"') ? "string" : "character";

            while ((ch = getcpp(ctx)) != EOF) {
                if (ch == sep)
                    break;;
                if (ch == '\\') {
                    if ((ch = getcpp(ctx)) == EOF)
                        break;
                }
                if (ch == '\n') {
                    fprintf(stderr, "%s:%d: unescaped newline in %s constant\n",
                            filename, ctx->lineno - 1, const_type);
                    /* This is a syntax error but keep going as if constant was terminated */
                    break;
                }
            }
            if (ch == EOF) {
                fprintf(stderr, "%s:%d: unterminated %s constant\n",
                        filename, startline, const_type);
            }
            flush_context(ctx, ft);
            continue;
        }
        if (ch == '_' || isalpha(ch)) {
            /* identifier or keyword */
            define_t *dp;
            while (isalnum(ch = getcpp(ctx)) || ch == '_')
                continue;
            ungetcpp(ctx, ch);
            for (dp = defs; dp; dp = dp->next) {
                if (dp->len == ctx->pos && !memcmp(dp->tok, ctx->buf, ctx->pos)) {
                    /* matching symbol */
                    fputs(dp->def, ft);
                    ctx->pos = 0;
                    break;
                }
            }
            flush_context(ctx, ft);
            continue;
        }
        if (ch == '.' || isdigit(ch)) {
            /* preprocessing number: should parse precise syntax */
            while (isalnum(ch = getcpp(ctx)) || ch == '.')
                continue;
            ungetcpp(ctx, ch);
            flush_context(ctx, ft);
            continue;
        }
        flush_context(ctx, ft);
    }
    if (outname) {
        fclose(ft);
    }
    free(ctx->buf);
    return 0;
}

int main(int argc, char *argv[]) {
    char *filename = NULL;
    char *outname = NULL;
    define_t *defs = NULL;
    FILE *fp;
    int i;

    for (i = 1; i < argc; i++) {
        char *arg = argv[i];
        if (*arg == '-') {
            if (arg[1] == 'h' || arg[1] == '?' || !strcmp(arg, "--help")) {
                printf("usage: pcpp [-o FILENAME] [-Dname[=value]] ... [FILE] ...\n");
                return 2;
            } else
            if (arg[1] == 'o') {
                if (arg[2]) {
                    outname = arg + 2;
                } else
                if (i + 1 < argc) {
                    outname = argv[++i];
                } else {
                    fprintf(stderr, "pcpp: missing filename for -o\n");
                    return 1;
                }
            } else
            if (arg[1] == 'D') {
                if (arg[2]) {
                    add_define(&defs, arg + 2);
                } else
                if (i + 1 < argc) {
                    add_define(&defs, argv[++i]);
                } else {
                    fprintf(stderr, "pcpp: missing definition for -D\n");
                    return 1;
                }
            } else {
                fprintf(stderr, "pcpp: bad option: %s\n", arg);
                return 1;
            }
        } else
        if (strchr(arg, '=')) {
            add_define(&defs, arg);
        } else {
            filename = arg;
            if ((fp = fopen(filename, "r")) == NULL) {
                fprintf(stderr, "pcpp: cannot open input file %s: %s\n",
                        filename, strerror(errno));
                return 1;
            }
            preprocess(filename, fp, outname, defs);
            fclose(fp);
        }
    }
    if (!filename) {
        preprocess("<stdin>", stdin, outname, defs);
    }
    return 0;
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
0

EDIT: This is a non maintainable solution - but it works. Don't use this if you expect your project to grow into several versions over time.

My attempt makes use of preprocessor conditional code and string concatenation (the fact that in C you can do "abc" "def"and it will be trated as "abcdef".

#include <stdio.h>

#ifdef V1
#define VERSION "1"
#define VALUE 99
#else
#define VERSION "2"
#define VALUE 66
#endif


int main(void) {
    printf("This file is in version " VERSION " and contains value %d\n", VALUE);
    return 0;
}

which prints

>> ~/playground/so$ gcc -DV1 q1.c 
>> ~/playground/so$ ./a.out 
This file is in version 1 and contains value 99
>> ~/playground/so$ gcc -DV2 q1.c 
>> ~/playground/so$ ./a.out 
This file is in version 2 and contains value 66



Fra93
  • 1,992
  • 1
  • 9
  • 18
  • This will quickly get super bloated and unreadable as new versions are released. Just use a single constant and handle it in a separate file. As a bonus, this also lets you tie a specific version macro to a specific number of source code changes through your version control system. No need to make things more complicated than they need to be. – Lundin Jul 12 '22 at 14:17
  • Good suggestion, I don't know if OP wants a quick and dirty solution or a maintainable one, so I'll leave it here adding your comment at the top. – Fra93 Jul 12 '22 at 14:19
  • Cool solution, but what I'm really looking for is the intermediate code, my users needs the original .c file without any mention of `VERSION` like it was always 1.0 or something. – badbouille Jul 12 '22 at 14:23
  • @badbouille Again, why can't you place `VERSION` in a separate file. – Lundin Jul 12 '22 at 14:24
  • @Lundin that's what I'm currently doing, but I'm looking to make the `VERSION` keyword disappear in the .c file, by executing the single `#define VERSION "1.0"` line – badbouille Jul 12 '22 at 14:32
  • @badbouille Sounds like what you are actually looking for is to run a search & replace script before compilation then. It's pretty common procedure and every programming IDE supports it. – Lundin Jul 12 '22 at 14:45
-2

Read about autoconf https://www.gnu.org/software/autoconf/

and maybe even about automaker (if you want to generate makefiles) https://www.gnu.org/software/automake/.

0___________
  • 60,014
  • 4
  • 34
  • 74