C Strings Saved My Code

Posted on Nov 28, 2021

C strings have the worst of reputations, don’t they? They’re claimed to be unsafe, cumbersome, just a hack based on pointers and not a real data type… You name it. And all of those things are kinda true? Yet they’ve helped me simplify scalc’s code in an unexpected way that I just needed to share with all of you guys, because it’s always fun to get into this sort of stuff, isn’t it?

So, what’s the deal?

At the time of writing this post, scalc has two independent modules that deal with commands and math operations the user may input; respectively, these are cmd.c and op.c. They rely on two different parsing techniques because the maths parser implements an RPN calculator, while commands need their arguments to be placed after the command name.

If you have a look at op.c, you’ll see that all operations share the same signature… because you don’t need them to have them anything different anyways. It’s all maths, you’re just doing stuff to double’s and returning a new double value. This allows for very simple ways of implementing many, many things… and even reusing subroutines for the C Standard Math Library which match this very same signature, as you may have already noticed from the op_defs array. That very same array, op(), and the apply_op() and eval_math() subroutines at scalc.c implement the magic that makes it possible to map the mathematical expressions to the right subroutine. Yeah, I know there is a lot to improve in there… and I wanna address that later in this post; be patient!

Commands, on the other hand… differ a lot in what they do and in the information they need to get in order to do stuff. You know, there’s a command that just shows the version of scalc you’re using and needs no arguments (i.e., :ver); there’s a different command that saves the value at the top of the stack and stores it into one of the 10 available memory “registers” of your choice… and the name of that register is an argument you pass to :sav.

The first implementation of this… well… you can see it yourself by checking out earlier commits… but it sucked enormously. In a nutshell, there were a couple of very arbitrary command “types,” similarly as to how operations use an enum type to tell how many arguments they need to take… While doing that for number of arguments follows a logic, the types I ended up declaring for commands were all over the place: :sav was a “memory” command (but also memory-related :mclr wasn’t), there were “stack” commands… It was a mess that severely limited everything.

So I figured out I wanted to be able to reduce all commands to one single signature if possible, but also retaining the ability to pass arbitrary numbers of arguments… and of any types possible…

After a very clueless attempt using void pointers,1 I got the inspiration to use “strings” from… well… C itself. You’re probably very familiar with this prototype, aren’t you?

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

If you have a look at scalc’s cmd.c code, though, you’ll notice that commands take a different prototype than main()’s:

int cmd_whatever(const char *args);

So it’s not exactly the same thing… but it sort of is as well. As we all should know by now, C doesn’t really have strings, but arrays of characters… however, these arrays become strings from a semantic point of view thanks to the null-terminator convention… which is just an arbitrary convention… and not even the only way to do it,2 but which makes a simple array somehow self-contain itself and, therefore, be just “more” than a pointer to a memory region where characters happen to be.

However, main() must work regardless of whether you’ve included any of the usual string-related subroutines that are used for parsing stuff, e.g. strtok() or sscanf(). That’s why argc is needed there and why each command-line argument is stored as an independent string, argv being a double pointer. This makes it possible to easily parse arguments without resorting to anything but a regular loop.

Yeah, of course, you do have getopt() to parse arguments in a fancy way with POSIX-compatible options… but that comes from unistd.h, it’s not in the C Standard, but in POSIX.

I can do things differently in scalc, of course. I’m building a project taking all I can from what C and POSIX offer me, so I can and should use string.h. This means I can use sscanf() to check what’s inside the arguments string, using spaces as delimiters (because that’s what the user inputs). And when I hit \0, the string is finished; no need for a variable telling me how long the string is.

Having all commands be of the same form simplifies eval_cmd() in scalc.c so much that I can paste the whole thing here and the post is still readable!

static void
eval_cmd(const char *expr)
	char expr_cpy[SCALC_EXPR_SIZE];
	char *expr_ptr;
	const CmdReg *cmd_ptr;

	strlcpy(expr_cpy, expr, SCALC_EXPR_SIZE);
	expr_ptr = strtok(expr_cpy, " ");
	cmd_ptr = cmd(expr_ptr);
	if (strncmp(cmd_ptr->id, "", CMD_ID_SIZE) == 0)
		goto printerr;

	expr_ptr = strtok(NULL, " ");
	if ((*cmd_ptr->func)(expr_ptr) < 0)
		goto printerr;


	fprintf(stderr, "%s: %s\n", expr, errmsg());

All scalc needs to do is to check the first element in the user input string, match it with the list of available commands, grab the function pointer using cmd(), and finally pass the rest of the string as the arguments string! Could this get more straightforward?

I must say I was really surprised at how neat this works. C strings have such a bad reputation, but I’ve always thought that’s pretty undeserved. OK, yeah, they only make sense if you know a bit of what’s going on under the hood, but they do make a lot of sense.

I know, I know… This whole project feels like reimplementing stuff that is available in higher-level languages out-of-the-box. Yeah, and that’s precisely it’s been SO much fun! I was always curious about how to do something like this and it feels really, really good to have found a working solution. I feel proud of this and I want to extend this to maths operations as well in the next couple of weeks.3 These are the perks of being a hobbyist coder: total creative freedom and not giving a damn about current trends 😂

  1. Don’t blame me; sometimes I’m such a naïve girl… ↩︎

  2. A popular alternative was/is(?) to prepend the string’s length to the string itself. It comes with its own set of problems and advantages… as everything does in programming! ↩︎

  3. I may release scalc 0.4 before that, though. I need to think a bit more about the release plan because I feel 1.x is very close! ↩︎