Scripting With C

C As A Scripting Language

(Joe Linhoff 12/2020)

A little over a year ago at work, we were talking about choosing a language for a few tools and small programs. We work mostly in C, but C was quickly ruled out because others didn’t see it as the best option. Python and Go were the top contenders because they were thought to be easier.

This upset me and made me think.

The problem isn’t anything about C itself, but with the boilerplate and build infrastructure. The multiple steps, commands, command line options, and tools needed to run and debug are the real problems.

InstantC makes it easier to write and run C programs. It moves the build steps and boilerplate into the background and lets you write and run C like a scripting language. InstantC is a new way to write and run C and C++ programs.

Hello Scripting

Scripts are run directly and don’t have additional build steps. For example, to run a Ruby script, create the file helloruby:

puts "Hello, Ruby."

Run the Ruby script:

$ ruby helloruby
Hello, Ruby.

Compare the Ruby one liner to the multiple lines of C. For example, with the file hello.c below:

#include <stdio.h>
int main() {
  printf("Hello, C.\n");
  return 0;
}

Build and run the C program:

$ gcc hello.c
$ ./a.out
Hello, C.

Both examples print a message. The example in C shows more code and steps to do the same thing. Traditionally, scripting languages combine the build and run steps, whereas compiled languages, like C and C++, keep the steps independent.

Hello InstantC

InstantC adds boilerplate, and builds and runs in one step.

For example, the file helloc has only the statement you want:

printf("Hello, InstantC.\n");

And, this is run in one step:

$ instantc helloc
Hello, InstantC.

Running this C program is a lot like running the Ruby script in the example above.

Taking this one step farther, on Unix-compatible systems, we can run the C program directly, like other scripts, by adding a special shebang line to the top of the file and making the file executable. The term ‘shebang’ stands for the two characters ‘#!’ at the beginning of a script.

Add the shebang to the top of the file, helloc:

#!/usr/bin/env instantc
printf("Hello, InstantC!\n");

The chmod +x helloc below makes the file executable. You only have to do this once.

$ chmod +x helloc

Now, your C program can be run directly:

$ ./helloc
Hello, InstantC!

InstantC adds boilerplate, runs the build tools, and runs the executable. The result is that you can write and run C as if it were a scripting language.

See the link at the end to the InstantC website for intallation instuctions.

What Makes C Hard

I have a number of thoughts about what makes C hard for new developers. There are the usual suspects like pointers, memory management, and minimal library support. However, these are not the first challenges. Often overlooked is what it takes to write and run even simple C programs. Compared to most scripting languages, C is more work.

My attitude for many years, was that the extra work needed to build and run C programs was OK, the benefits far outweigh the costs, and mastery of the extra steps made you a real programmer. My attitude has changed in that now I’d rather see more developers learning and using C, even if they haven’t mastered all the ancillary necessities.

The C code in the earlier hello.c example includes multiple lines that are ancillary and necessary to the printf statement. In this case, the #include <stdio.h> the int main() { with the top level curly-braces return 0; } are what I consider ancillary and necessary boilerplate. This boilerplate is needed to build and run the single printf statement; which is really all you want. Understanding the boilerplate is a hurdle to many developers.

Also in the example, we used gcc to compile the program to a.out. These are two more things to learn, ancillary to our print statement.

This is a simple example. In short order, we will add more to the build process with compiler options, a build script, or possibly even introduce make. These tools are at the foundation of C development and undisputedly important. The problem is that they can get in the way of quickly creating simple programs.

Instant Arguments

The goal of InstantC is not to create a new scripting language. On the contrary, InstantC doesn’t change any of C’s syntax. It uses meta-substitutions on your code, reorganizes, adds boilerplate, and rewrites the file before compiling.

For example, to get the arguments passed into your script, use the meta-substitutions $(ARGC) and $(ARGV) for the argument count, and the array of argument values, as in the example helloargs below:

#!/usr/bin/env instantc
for(int i=0; i<$(ARGC); i++)
  printf("Hello argv[%d]:%s\n",i,$(ARGV)[i]);

Make the file executable, $ chmod +x helloargs, and run:

$ ./helloargs 123 abc
Hello argv[0]:./helloargs-sc.exe
Hello argv[1]:123
Hello argv[2]:abc

As you see, arguments 1 and 2 are arguments the user is passing into your script. Argument 0 is the path to the executable that was built by InstantC. InstantC scans your source file and replaces $(ARGC) and $(ARGV) with the actual variable names before compiling your code.

Instant Functions

For most developers, C programs start in main. Likewise, InstantC builds your code into the main function. To create functions, add the $(functions) meta-token, and write new functions after that token.

For example, create the file hellofunctions:

#!/usr/bin/env instantc
  int a = square(16);
  printf("square:%d\n",a);

$(functions)
int square(int x) {
  return x*x;
} // square()

And, run this:

$ ./hellofunctions
square:256

InstantC adds the needed boilerplate and reorganizes the section with the functions to appear before main. The functions section is also a place to add constants or variables shared by multiple functions.

Instant Debugging

Debugging code is like proofreading an essay by reading it outloud. Debugging is the process of watching and interacting with your code as it runs. You start your program up ‘inside’ the debugger, gdb in this case, and step through code, inspecting values, watching how your code handles itself, thinking through possible problems and ways to improve it. When writing good code, an interactive debugger is an important tool.

InstantC makes it easy to debug. Start up your program, with the +debug option:

$ ./hellofunctions +debug
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
...
Starting program: /mnt/d/abookperformance/instantc/examples/hellofunctions-sc.exe 
Breakpoint 1, main (_scargc=1, _scargv=0x7ffffffee488) at ./hellofunctions-sc.c:9
9       int _scmainrc=0;
(gdb) 

InstantC builds and launches your program inside the debugger. From here, you type in and run gdb commands.

Here are a few GDB commands:
n ENTER -- to run the next line
s ENTER -- to step into the next statement
p VAR ENTER -- to print the value of VAR
q ENTER -- to exit the debugger

Instant Pinpoint Debugging

I added ‘pinpoint’ debugging to make it easier to debug the lines of code you’re currently working on. Add the meta-token $(gdb break) to the line you’d like the debugger to break on, and run your code with the debug option.

For example, add $(gdb break) inside the square function:

int square(int x) {
  $(gdb break)
  return x*x;
} // square()

Run this example with the debugger, and print the value of a variable:

$ ./hellofunctions +debug
...
Breakpoint 1, square (x=16) at ./hellofunctions-sc.c:5
5         return x*x;
(gdb) p x
$1 = 16
(gdb)

InstantC launches your program with gdb. When the breakpoint is hit, a gdb prompt appears and waits for you to type a gdb command. In the example above, I type p x and hit return to print the value of x.

Pinpoint debugging helps you quickly debug the exact code you’re working on. When I write a new routine, I move the break to that routine, and step through the code. Adding this meta-token does not add any code to your program and is ignored unless you run with the +debug option.

Features

There are a few big features I’ve worked into InstantC that I’d like to mention in this article. Visit the InstantC website for more documentation.

Lazy Compilation

First, your code is compiled into the executable only when needed. The second time you run your code, InstantC skips the build step and runs the executable. This gives you a scripting environment with compiled-C execution times.

With the caveat that timing tests are hard to get right, I’ve shown the results of some timing tests below. We see a sizable improvement in the second running of the helloc program because the build step is skipped.

Time to run Ruby:

$ /usr/bin/time --format='%C took %e seconds' ruby hellorb
Hello Ruby
ruby hellorb took 0.11 seconds

Time to build and run C:

$ /usr/bin/time --format='%C took %e seconds' gcc hello.c && ./a.out
gcc hello.c took 0.13 seconds
Hello, C  

Time to build and run InstantC:

$ /usr/bin/time --format='%C took %e seconds' instantc helloc
Hello C   
instantc helloc took 0.21 seconds

Time to run InstantC after the first time:

$ /usr/bin/time --format='%C took %e seconds' instantc helloc
Hello C   
instantc helloc took 0.02 seconds

Libraries

The second big feature is the ability to create libraries with tests. Compared to how you may normally do this, libraries are easier to create with InstantC. These libraries can be used by any C/C++ program, not just other InstantC programs. I added this capability to InstantC for two reasons: because I’ve always found libraries hard to create, and also to add unit-test-like tests to the build processes itself.

The code in your library-building InstantC file gives you a chance to run tests on your library during its build. If you return an error code, the library will not be built. These tests, like unit-tests, are useful to both test the library itself, and also provide documentation for users of the library.

Beta Code

Another big feature I added to InstantC is what I call ‘beta’ code. This code is compiled and run at compile time before your primary code is compiled. The output of the beta code is added and compiled into your primary program.

This gives you a way to generate code and operates in the area of macros, templates, and constant-expression code; however, beta code is just normal C/C++ code, run at build time, that has access to the definitions in your program.

C++ Support

Finally, although this article focuses on C, InstantC builds and runs C++ programs in the same way. To signal the program is C++, either run with a file extension that contains ‘cc’ or ‘cpp’, or add the meta-token $(lang c++) to your code.

For example, create the file hellocpp:

#!/usr/bin/env instantc
$(lang c++)
std::cout << "InstantC++ Hello!\n";

Run the code:

$ ./hellocpp
InstantC++ Hello!

Goals

My goal for InstantC is not to create a new scripting language. My goal is to make C easier to use and provide a platform for creating quick scripts, tools, and programs. InstantC doesn’t parse or understand the language itself, but works on the simpler principle of meta-substitutions. In a good way, this makes InstantC mostly independent of the language and allows that updates of the underlying tool chain do not cause problems for InstantC code.

Summary

I’ve enjoyed working on InstantC and it fits a number of needs I have. At work, I use it for tools and testing. I often pull a routine out of another project and test it quickly using InstantC. I also find it useful to demonstrate code to others.

InstantC is a work in progress. I work mostly on Linux systems, and have lightly tested it on Cygwin. I know that different configurations and users will uncover issues, please send me feedback.

I hope you find InstantC useful.

Copyright (C) 2020-2022 Joe Linhoff - All Rights Reserved