Presenting GCCalc: a horrible abuse of GCC

Following an interesting discussion on Reddit about first class functions in C, I was inspired to see what I could do with this new-found knowledge. The result is what I affectionately call “GCCalc”, for reasons that will become clear below.

GCCalc is a simple command line calculator, much like the common bc calculator on many Unix systems. It’s implementation, however, is very different than most calculators. While bc is said to have “C-like syntax”, GCCalc’s syntax is C. Whatever you enter on the command line automatically gets compiled, loaded, and executed, and the result is returned (as a double) and printed to the screen.

You can either enter expressions like:

round(46.95886*sqrt(1+2/9.99*sin((21%5)*pow(2,8))))

or you can enter whole C statements (as long as they’re on one line, for now) like:

 int i; for (i=0;i<10;i++) { printf("hello world!\n"); } printf("goodbye\n");

Unfortunately variables are scoped to the function that wraps them, so they don’t persist across multiple entries. However, you can access the last result using the “last” variable (a double).

Here’s the source file, and here’s a syntax highlighted version:

It’s been tested on Mac OS X (Leopard) and Linux (Ubuntu Gutsy), with GCC 4. Compile with “gcc -o gccalc gccalc.c” on OS X, or “gcc -o gccalc gccalc.c -ldl” on Linux.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>

#ifdef __ELF__
#define GCC_FLAGS "-fPIC -shared"
#define EXTENSION "so"
#else
#define GCC_FLAGS "-dynamiclib"
#define EXTENSION "dylib"
#endif

#define HEADERS "#include <stdio.h>\n#include<math.h>"

typedef double(func_return_double)(double);

unsigned count = 0;
char *cwd;
char tmp_path[1024] = {‘\0′};

void *lib = NULL;

int main(int argc, char **argv)
{
    double result = 0.0;
    char input_buffer[1024], code_buffer[2048], function_name[32], command_buffer[1024];
    
    // get out current directory, which we’ll use for tmp files (dlopen seems to need absolute paths)
    cwd = getcwd(NULL, 0);
    
    while (1)
    {
        // for unique function and file names (needed for dlopen/dlsym to work correctly)
        count++;
        
        // read in the next line
        printf(">> ");
        fgets(input_buffer, sizeof(input_buffer), stdin);
    
        // format the function name
        sprintf(function_name, "f%d", count);
        
        // format the code string: if it doesn’t contain a semicolon, assume it is just an expression
        if (strchr(input_buffer, ‘;’))
            sprintf(code_buffer, "%s\ndouble %s(double last) { %s\nreturn 0; }", HEADERS, function_name, input_buffer);
        else
            sprintf(code_buffer, "%s\ndouble %s(double last) { return (%s); }", HEADERS, function_name, input_buffer);
            
        // format the filename string, delete the file if it exists
        sprintf(tmp_path, "%s/libtmp%d.%s", cwd, count, EXTENSION);
        unlink(tmp_path);
        
        // format the gcc command string
        sprintf(command_buffer, "gcc -Wall %s -x c – -o %s", GCC_FLAGS, tmp_path);
        
        // execute gcc command, write out the code
        FILE *fp = popen(command_buffer, "w");
        fwrite(code_buffer, 1, strlen(code_buffer), fp);
        fprintf(fp, "\n");
    
        // pclose waits for gcc to terminate (fclose/close do NOT thus compilation will sometimes not finish prior to the dlopen)
        pclose(fp);

        void *ptr = NULL;
        
        // open the just-compiled dynamic library
        if ((lib = dlopen(tmp_path, RTLD_NOW|RTLD_LOCAL)) == NULL) {
            puts(dlerror());
        }
        // get the function pointer
        else if ((ptr = dlsym(lib, function_name)) == NULL) {
            puts(dlerror());
        }
        
        // execute it
        if (ptr != NULL)
        {
            func_return_double *func = (func_return_double*)ptr;
            result = (*func)(result);
            // print the result
            printf("=> %.*lf\n", (result/((int)result)>1.0)?5:0, result);
        }

        // clean up: close the library, delete the temp file
        dlclose(lib);
        unlink(tmp_path);
    }

    return 0;
}

Thanks to jbert on Reddit for the initial code and inspiration.

If only I had known about this back when The Daily WTF has having their OMG WTF crazy calculator programming contest…

  • This would be cool ported to LLVM's new clang C compiler. Since everything in the LLVM world is a library, it could do everything in-process and not have to shell out to the compiler.

  • Anon

    http://neugierig.org/software/c-repl/

  • Eelis

    Geordi ( http://www.eelis.net/geordi ) does more or less the same for C++, but is primarily an IRC bot (and consequently has to take severe security precautions).

  • Tom

    Thomas Ptacek:


    Yeah I don't do much checking of the input, so there's certainly the risk of buffer overflows... but then again, the program gives the user the ability to compile and execute arbitrary C code, so it's not something you want running as a network service, etc...

  • I like your "working buffer is twice the size of the input buffer so sprintf is fine" buffer management strategy. Capturing the essence of C in your C calculator is a great thing.

  • jil

    Well, one missing piece of information on the last comment.. the gcc was 4.2.1.


    Then tried with
    OSF1 foo V5.1 2650 alpha
    and
    gcc version 3.4.1


    which won't even compile.


    Finally tried with
    Linux foo 2.6.20-1.2948.fc6 #1 SMP i686 i386 GNU/Linux
    and
    gcc version 4.1.2


    and it runs... ...except when you press ctrl-D (or pass /dev/null as an input):


    Segmentation fault


    Other than that, gccalc is a nice piece of sw.

  • jil

    SunOS foo 5.10 Generic_125100-10 sun4u sparc SUNW,Sun-Fire-V240


    compiles ok, but segfaults when trying to calc the answer.

  • whiney passer-by

    Damn, those floating icons are annoying. What an awful design!


    Simple to solve: turn off styles!

blog comments powered by Disqus


Warning: include(/home/tlrobinson/tlrobinson.net/_footer-analytics.php) [function.include]: failed to open stream: No such file or directory in /home/tlrobinson/tlrobinson.net/blog/wp-content/themes/clean-look-150/footer.php on line 13

Warning: include() [function.include]: Failed opening '/home/tlrobinson/tlrobinson.net/_footer-analytics.php' for inclusion (include_path='.:/usr/local/lib/php:/usr/local/php5/lib/pear') in /home/tlrobinson/tlrobinson.net/blog/wp-content/themes/clean-look-150/footer.php on line 13