msoucy.me

Code, games, and sarcasm
Matt Soucy

Entry Point

in code by Matt Soucy - Comments

Recently, I found myself getting frustrated at the lack of control I had over shell scripts. Many times, I caught myself realizing that a single C function could do all that I needed.

At first, I debated writing a quick C (OK, C++) script to do what I wanted:

:::c++
#include <iostream>
#include <cstring>

int main(int argc, char** argv)
{
	// The length should be printed to stdout, not used as the "return value".
	// This allows us to handle "error cases"
	if(argc != 2) {
		return 1;
	}
	// An error in output is still an error
	return !(std::cout << strlen(argv[1]) << std::endl);
}

This worked fine for just one function, but I had to write a new (though very similar) program every time. For example, here is pow:

:::c++
#include <iostream>
#include <cmath>
#include <cstdlib>

int main(int argc, char** argv)
{
	if(argc != 3) {
		return 1;
	}
	return !(std::cout << std::pow(atof(argv[1]), atof(argv[2])) << std::endl);
}

What do these have in common? The following python might help:

:::python
def some_func(func, args, argtypes):
	funcargs = [argtype_to_type(t)(a) for a, t in zip(args, argtypes)]
	return call_c_function(func, *funcargs)

Every time I wanted to use one of these functions, I would have to parse the argument as the correct type, and then print out the output. I could cheat slightly because, up to this point, there was no real chance of having complex (read: nonprimative) types involved.

Was there a way to make this generic? I would say so.

The first piece of the puzzle was handling parsing. 99% of the time, this should be sufficient for “parsing”:

:::c++
#include <sstream>
template<typename T>
T conv(const char* text)
{
	T ret;
	std::istringstream(text) >> ret;
	return ret;
}

Since we’re already passing the type, why not special-case some of the builtins? To avoid issues with improperly formatted strings, we’ll use the C++11 stoi and family to force it to throw. Naturally, we can only use this if C++11 is enabled:

:::c++
#if __cplusplus > 199711L
template<>
int conv<int>(const char* txt)
{
	return std::stoi(txt);
}
#endif

(Since we have a clear mapping of types to these functions, we can just use a for loop when generating the C++ later)

Now that we have the convenience functions, we just need the main body:

:::c++
// Main for the `pow` executable
int main(int argc, char** argv) {
	if(argc-1 != 2) {return 1;}
	double arg1 = conv<double>(argv[1]);
	double arg2 = conv<double>(argv[2]);
	std::cout << pow(arg1, arg2) << std::endl;
	return 0;
}

This is fairly generic - most of this can be autogenerated. First, the program needs to test for the proper return value. Then, it needs to convert the arguments into appropriate types. Then it needs to print the output of the function and return a success.

Unfortunately, it has some issues so far. Any function that operates on C strings (strcat, for instance) is dangerous because it might not have the appropriate amount of space allocated. It also doesn’t handle any errors, variadic functions, or C++ class methods.

We can fix the error handling easily:

:::c++
int main(int argc, char** argv) try {
	if(argc-1 != 2) {return 1;}
	double arg1 = conv<double>(argv[1]);
	double arg2 = conv<double>(argv[2]);
	std::cout << pow(arg1, arg2) << std::endl;
	return !std::cout.good();
}
catch(...)
{
	return 1;
}

Mutable char* can be fixed by allocating:

:::c++
int main(int argc, char **argv) try {
	if (argc - 1 != 2) {
		exit(1);
	}
	char *arg1 = conv<char *>(argv[1]);
	char *arg2 = conv<char *>(argv[2]);
	std::cout << strcat(arg1, arg2) << std::endl;
	// More generated lines
	delete[] arg1;
	delete[] arg2;
	return !std::cout.good();
}
catch (...) {
	return 1;
}

For convenience, we can wrap the allocation in a simple class.

This is as much progress as I have made on the C++ side, though there are a few changes I could make:

The code will be available at my code repository.