errno == -ERIC

Werking With The LC-3 Again

Hello LC-3 My Old Friend

A while ago I stumbled on a fun post entitled, Write Your Own VM1, which detailed the simplified components of a virtual machine and how to approach emulating the LC-3. I hadn't touched LC-3 since I was in ECE 190 which was [redacted] years ago. With my interest piqued, I decided I would go through this exercise. The post does a great job of splitting up the main components of work into manageable sections which made jumping in very approachable.

A Brief Reflection on the LC-3

Feel free to skip my silly diversion As I mentioned, the LC-3 was the architecture used to teach ECE 190 in my freshman year at UIUC. At this point in time, I was a very reluctant programmer. I was insistent that I would do hardware, I was in Electrical Engineering after all. ECE 190 is the first moment where my grade bifurcation began: anywhere from bad to pretty good in my EE classes, and good to great in my CompE classes. Professor Kumar, I apologize for any agita I caused with my shunning reality 😅

But even then, I still greatly enjoyed working with the LC-3. I came out of ECE 190 in awe of the power of assembly! For some reason I thought C was too overwhelming, which lol... But I loved the architecture and learning the guts inside of the machine was illuminating. I remember discussing with a friend about how cool instruction sets were! I never ventured into anything beyond but the spark was there.

My disdain for the digital arts is deeply funny in hindsight. Despite my every attempt to not code, it kept happening! I chose EE-focused rotations and I still was the guy to make a programmer for some NOR Flash to track component usage, control a 6-axis platform, or some other prototype. Thankfully maturity began to kick in and I relented. I've been pretty happy going down this path since then.


Now that we've returned from memory lane, I can get to why I wanted to write this post. Every time I start a new C project, I'm immediately presented with the awkward state of C tooling: there's none... When you start a C project today, your best bet is GNU Make or CMake and then that's about it. These tools are fine in the grand scheme of things, but they both have their drawbacks:

I also considered some of the newer build systems like Meson or Bazel, but ultimately I didn't want to invest a ton of effort. This was a little hobby project after all. By chance, I stumbled upon a new build system/task runner by the name of Werk and after a quick perusal I liked what I saw. It was time to get to Werk2!

Werking Hard Or Hardly Werking

After a little bit of time to learn the tool, I am very please with how much I enjoyed using Werk for this project. I think Werk takes the best parts of Make/CMake and simplifies things into an approachable form. I'll start with the parts that I liked the best:

Werk Docs

Werk has a nice set of docs that let users quickly pick up the basics of the build system. I also appreciated that the author has outline what the advantages and disadvantages of the tool are. I think this honesty is very good because other build tools don't let you know where the traps are which can be frustrating after you've completely reworked your project for them.

Werkfiles

Werk uses a Werkfile to determine how to build your project. The syntax is pretty straight forward and consists of variable, build, and task definitions. Variables are what you would expect, anything from a static string to shelling out to determine where a package might be. Build definitions tell werk that this task produces some sort of output. Tasks are similar to .PHONY targets in Make, they should always be run if the target or a dependency.

Compared to Make/CMake, defining tasks is very clear. No needing define something as .PHONY, no confusing add_custom_command/add_custom_target pairs needed. The commands needed to run the task are simply placed in the block and that's that!

Deps of Despair (I Kid, They're Easy!)

Another thing I really liked was that considerations for depfiles are part of werk. CMake generally handles this for you thanks to it's Ninja/Makefile backends, but for Make this was always a frustrating part for me. I don't want to have to define rules to make these or figure out where to plumb it in. Here's how Make suggests to define rules for the .d files:

%.d: %.c
        @set -e; rm -f $@; \
         $(CC) -M $(CPPFLAGS) $< > $@.$$; \
         sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$ > $@; \
         rm -f $@.$$

# Snipped for brevity

include $(sources:.c=.d)

vs Werk

build "src/%.o" {
    from "src/{%}.c"
    depfile "{in}.d"

    # Snipped for brevity

    # Generate depfile and object file in the same command
    run "{cc} -MMD -MT <in> -MF <depfile> -c {flags*} -o <out> <in>"
}

In werk, it's part of your build rule. Part of my aggravation using Make over the years is that so much of the tool relies on implicit rules and hard to decipher syntax. I'll be honest, I don't like shelling out to sed/awk/xargs. I don't like needing to remember if I should use $@ or $^ or even where in the manual to find those definitions3.

A highly enjoyable part of using Werk came when I hit snags. The tool has very descriptive error and debugging output and it's clear syntax makes finding problems much less of a headache. Since the project states these upfront, I felt prepared to tackle to corner-cases for my setup. There are two places that I hit some problems:

Problem 1: Downloading/Building Dependencies

My preferred project setup is to have a manifest of some sort, pull in dependencies and build them along with my source code. This way I can track versions of these packages/libraries but also not include them as a submodule (fine but annoying) or add the source to my repo (🤢). To this end, I ended up with a task definition that I can run as needed to pull down dependencies. Then I created a build definition to shell out to a helper script to build CppUTest as a static library. CppUTest already contains Make/CMake scripts so without re-inventing a Werk-based wheel this seemed like a happy medium.

task get_deps {
    let script = "scripts/get_deps.sh"
    run {
        shell "zsh {script}"
    }
}

build "lib/libCppUTest%.a" {
    let script = "scripts/build_cpputest.sh"
    run {
        shell "zsh {script}"
    }
}

Improvements

Since tasks are re-run every time, I did not tie get_deps in as a dependency to any other definitions. I think I could get this done with touch deps_cloned and have that be the output of a build definition but it works for me for now.

Problem 2: Running Tests

Since I setup my tests as a single executable, it was simplest for me to include all source files and all test source files. The problem is that this makes mocking components difficult in C. I could have refactored things to not directly call the functions to be mocked but it's a simple enough project I didn't want to add more abstraction. I settled for some #if !defined(CONFIG_TEST) blocks to help out. To incorporate this into Werk, I had two options:

I opted for the profile route as I had already plumbed that variable. To run my tests, I just need to use werk test -D profile=test. I'm sure there are other ways here as well

Improvements

I think my encounter here is more due to my testing/build setup than a shortcoming of Werk. Honestly, probably nitpicking my own setup too much, it's very doable and not that complicated compared to setups I've used in the past.

Next Steps

I've since completed the VM work but have some bugs to deal with. I've been using the 2048 application provided in the post1 to test my VM. My ultimate goals are (in no particular order):

Project Repo

Github: https://github.com/ejohnso49/lc3-vm

Acknowledgements

Just want to quickly thank Justin Meiners and Ryan Pendleton for their neat LC-3 VM project and post. It's been fun revisiting this! Thank you to Simon Ask Ulsnes for Werk. I really enjoyed using this as my build system/task runner


  1. Write Your Own VM by Justin Meiners & Ryan Pendleton

  2. Dad Joke Practice

  3. They're in 10.5.3 Automatic Variables for anyone else who forgets

  4. OCRE Project

  5. OCRE on Zephyr's Blog

#build-systems #lc3-vm #programming #werk