Tuesday, September 10, 2013

.dll? That's .so Microsoft. (C)

I was thinking about DLLs from the previous blog by Ken and decided to look a little at how shared objects are made and used in Linux. Based on this answer to a StackOverflow question, I came up with this example in C.

Preparing the shared object

Say you want to write a function sumSquare(int, int) which takes two integers and returns the sum of their squares. Then you could code it simply as follows:

    int sumSquare(int first, int second) {
        return ((first * first) + (second * second));
    }

Save this file as, say, sumSquare.c. Then you could build the shared object with gcc like this:

    $ gcc -fPIC -Wall -shared -o sumSquare.so sumSquare.c

where -fPIC instructs gcc to produce position-independent code which is necessary for code that can be in different locations, -shared compiles the code into a shared object instead of an executable, and -o filename specifies the preferred output filename. If this last argument is not given, then gcc prefers to use a.out as the output filename. -Wall is just my personal preference to show all Warnings.

Now you have the shared object which you can include in your code. Yeah... how?

Including the shared object

Now I write a tiny piece of code that takes the sum of squares of two numbers - say, 3 and 4 - and prints them on screen.

    #include 
    #include 

    int main(int argc, char** argv) {

        void *handle;
        int (*ss)(int, int);
        char *error;

        handle = dlopen("./sumSquare.so", RTLD_LAZY);
        if (!handle) {
            fprintf(stderr, "%s\n", dlerror());
            return 1;
        }
        dlerror();

        ss = dlsym(handle, "sumSquare");
        if ((error = dlerror()) != NULL) {
            fprintf(stderr, "%s\n", error);
            return 2;
        }

        printf("3^2 + 4^2 = %d\n", (*ss)(3, 4));

        dlclose(handle);
        return 0;
    }

OK, that wasn't so tiny after all. But there is good reason for it. Let's look at it segment by segment: first, the includes. <stdio.h> is responsible for input and output, and you might have seen it before. Then there is <dlfcn.h>. What does this library do? It handles the communication with the shared object, providing calls like dlsym(), dlopen() and dlerror().

Next, the variables are declared. A void *handle, seemingly a pointer to nothing, is followed by a strange int (*ss) which takes two arguments and a char* error. The last is easy enough to understand, it's a 'string', a sequence of characters.

The next block provides a, yes, handle on the shared object. ./sumSquare.so means that the shared object should be in the same directory as the code. The if-block prints an error to STDERR and returns 1 if nothing is in handle. The final dlerror() silently drops any stray errors. Not really the best approach, as you might guess... but I'll leave it to you to improve on :)

Then ss is assigned the symbol "sumSquare", which as we saw before, is the name of the function we want. Here again is an if-block in case an error pops up. Here, dlerror() will be NULL unless something goes wrong, so we check for that.

All that was just making sure the function could be accessed. Finally, the call to ss is made with the requisite (here hard-coded) arguments as part of a printf statement.

At the end of the function, the handle for sumSquare.so is closed.

Here is the call to the compiler:

    $ gcc -Wall -o trivial -ldl code.c

-ldl is necessary for the library in <dlfcn.h> to be linked in. This makes all the dynamic loading possible.

That's it! You can take a look at sumSquare.c and code.c if you wish. I would recommend reading about the dlopen() calls though.

No comments:

Post a Comment