home · posts · about · github

Arpan Dhatt / Doing C Assignments in Rust Instead

Sun Feb 6, 2022 · 1377 words · 7 min

I do not actually recommend doing university assignments in a language other than the one required by the class unless you have explicit permission by the professor. If you decide to actually do what I have written in the blog post, do so at your own risk. Despite the fact the content in this post explains how to write these assignments in almost any language of your choosing, I still did mine in C and plan to do all the assignments in the way instructed by my professor.

Introduction

As a part of the Computer Organization and Architecture course at college, I have to complete per-week programming assignments in C. For those of you who know me, I'm not a big fan of C. The language and its syntax is simple and to-the-point, which is nice. However, there are so many ways to shoot yourself in the foot with such a memory unsafe language that offers little to no abstraction, even if you want it. For that reason, using something like Modern C++ or Rust would be a better choice in a lot of (but not all) scenarios. Either way, this post isn't about the differences between C and Rust, although I would love to wax on about it to anyone who will listen. If you want to know right now what to do. Everything from now until then is how I went about exploring the capabilities of Gradescope's grading system and our particular grading script's architecture. If you are a UT student, a portion of this post is a sneak peek into a small part of CS439.

First Try: Dynamic Libraries

Going in, I already knew that Gradescope used Docker containers running x86_64 Linux to run the grading script provided by the instructors. Other than that, I'm not fully aware of all the limitations that Gradescope adds on top of these Docker containers. Perhaps it restricts network access, which would be prudent for a lot of university assignments, but I'm not entirely sure.

Since I knew the OS and architecture of these Docker containers, I could pre-compile an executable and deliver that code through the GitHub repository I turn in. This would only work if there wasn't any filtering of submitted files. Luckily, there wasn't. The first idea that came to mind was a dynamic library. In a nutshell, dynamic libraries are program binaries that cannot be run on their own, but a different program can open them, read symbols from them, and execute functions that were compiled into that dynamic library. This is a commonly used feature of modern OS's that allow updating portions of programs independently and allow sharing compiled code between multiple programs, reducing memory usage. Using them is quite simple. First, I needed to create a Rust crate that exported a C-compatible function. I made some small changes to the default Cargo.toml and added the following code:

#[no_mangle]
pub extern "C" fn fib(n: libc::uint32_t) -> libc::uint32_t {
    let mut b2 = 0;
    let mut b1 = 1;
    for _ in 0..n {
        let temp = b1 + b2;
        b2 = b1;
        b1 = temp;
    }
    b2
}

After compiling this code to a C-compatible dynamic library, I can access the fib function using the following C code:

#include <stdio.h>
#include <dlfcn.h>

int main() {
    void* handle;
    unsigned int (*fib)(unsigned int);
    // open the dynamic library so we can get functions
    handle = dlopen("./libfib.so", RTLD_LAZY);
    if (!handle) {
        printf("Failed to load dynamic lib\n");
        return EXIT_FAILURE;
    }
    // bind our fib function pointer to the fib symbol from this library
    fib = dlsym(handle, "fib");
    if (!fib) {
        printf("Failed to load fib symbol\n");
        return EXIT_FAILURE;
    }
    printf("fib(%d): %d", 8, (*fib)(8));
    return EXIT_SUCCESS;
}

The final step to getting this to work is simple, yet the reason why this approach doesn't work for our grading system. On Linux, we don't automatically get access to the dlfcn.h header. We need to link an additional library by passing the C compiler the -ldl flag. Only then can we use functions like dlopen and dlsym. This is simple enough on our end. We just need to add -ldl to the appropriate line our Makefile. Unfortunately, the grading script uses its own Makefile which won't link the needed library. But, what if I told you, there was another way...

First Try (Part 2): The UNIX Process API

Ok, so if I couldn't dynamically load a Rust binary, could I just run it as an executable instead? To try this out I needed to make use of the fork, execvp, and waitpid UNIX functions. Only the execvp function is necessary if you wanted to code your assignment in a different language, but I needed to use all three for testing purposes. I won't explain in detail what all of these functions do, but if you want to learn more I recommend checking out this interlude in Operating Systems: Three Easy Pieces.

At this point in time, I had already completed the assignment so if I could use that to test whether or not what I was doing would cause an error or not. I haven't made a Rust port of the entire assignment so I couldn't exactly just use that to test. That would be quite a lot of work and I'm a pretty lazy person. That's where fork and waitpid came in. In short, I can use fork to create a child process that "turns into" the Rust program using execvp. In the parent process I could use waitpid and a provided macro to make sure everything proceeded as planned. If my code passed all the test cases in Gradescope, it means nothing errored out. If it failed, that meant the new code I added didn't work.

This time there's no Rust code to share since the code could be literally anything that can be compiled to an executable. The C code is slighlty more involved, however:


#include "ci.h"
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[]) {
    // creates a copy of this process: one parent, one child
    pid_t pid = fork();
    // at this point on there are two identical processes
    if (pid == 0) {
        // fork returns 0 on the child process so this
        // must be the child process
        char* myargs[2];
        // this is an executable binary that I ended 
        // with .c just in case hehe (might not be necessary)
        myargs[0] = "./funny.c";
        myargs[1] = NULL;
        // execvp turns this process into the one about
        // to be loaded, aka my Rust code :)
        execvp(myargs[0], myargs);
    }
    int status;
    // wait until my Rust code finishes executing
    waitpid(pid, &status, 0);
    if (!WIFEXITED(status)) {
        // if the child process didn't run or was 
        // not successful the program ends prematurely
        // failing all test cases
        return 0;
    }
    // actual ci.c code provided in handout
}

With my breath held, I submitted this code to Gradescope and waited... 4.0/4.0! It worked! The child process was successfully created, turned into the pre-compiled Rust executable, and exited. The parent process saw this and continued executing as normal.

So What Do I (Not) Do?

The fork and waitpid commands are not necessary if you write your entire assignment in a different language. You can pre-compile the entire assignment into an executable and run it in the grader like this:

#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[]) {
    char* myargs[2];
    myargs[0] = "./theassignment.c";
    myargs[1] = NULL;
    execvp(myargs[0], myargs);
}

This should work perfectly fine. You could write the assignment in Rust, C++, Go, or Swift, or basically anything that compiles to an x86_64 Linux executable. If you want to write it in Java (just why?) or something else that requires a runtime, it's a bit more complicated. Unless Gradescope's Docker images already contains the runtime you need, you will need to bring it yourself. Maybe you can just package the entire node runtime with your assignment if you want to do it in JavaScript or TypeScript, I don't know. I haven't tried it.

And so, that's the end of this post. To whom it may concern, don't try doing your C assignment in C# (you know who you are).


home · posts · about · github