Evil #include-ing Python Scripts
🔗 The Problem
You wrote a beautiful lil’ Python script.
You run it on its own sometimes, but you’d also like to dispatch it from some C++ code.
We could just std::system()
the script from wherever it resides, but then we’d have to keep track of where it resides and make sure it’s available everywhere we want to execute our executable
We could always #include
the text of the script file into our source.
But we would want to shove it inside of a string.
In a perfect world, a raw string literal, which lets you shove arbitrary text into a string into without worrying about escaping, would be the ideal vehicle for these shenanigans.
Unfortunately, for a whole host of very depressing reasons, we can’t #include
into a raw string literal.
(Among other reasons, the #include
directive would be interpreted as, well, raw text.)
Wanting to embed data into C++ source is a common-enough concern that there are plenty of well-tread workarounds.
We could use xxd
to generate a header with the script inside a unsigned char[]
, but keeping that autogenerated header in sync with the script source would complicate our build process.
There’s some finagling in the standards committee that might eventually make this problem less annoying, but at best that’s a ways off.
🔗 The Solution
Turns out that Python also has raw string literal syntax.
Conveniently, these strings also start out with R"
.
Also, Python is perfectly happy to ignore any loose strings you may have floating around. For example,
print( 'foobar' )
'this string gets ignored'
print( "boop boop it's python time" )
Indeed, Python will happily ignore any loose strings, say, you might happen to position the beginning and end of a script.
See where this is going?
🔗 The Abomination
Where it is going is that we’re going to shove the C++ raw string literal syntax into our python script and then #include
that.
example.py
:
R"delimiter(" # allows us to #include this script into C++ source
import sys
# doing all my normal python stuff
def greet(who):
print( "hello", who )
if __name__ == '__main__':
__, who = sys.argv
greet(who)
# allows us to #include this script into C++ source
")delimiter"
main.cpp
:
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <string>
#include <system>
// contains stript as a string, with an extra " character at beginning/end
const std::string raw_script_source{
#include "example.py"
};
// temporary file we'll dump script into
const std::string script_path{
std::tmpnam(nullptr)
};
int main() {
// dump script into temporary file
{
std::ofstream out( script_path );
// drop leading and trailing "'s from raw_script_source
out << raw_script_source.substr(1, raw_script_source.size() - 2);
}
// construct command we'll pass to std::system
std::stringstream ss;
ss << "python3 " << script_path << " m_lady";
const std::string command{ ss.str() };
// dispatch python script
std::system( command.c_str() );
}
Proceed directly to hell. Do not pas Go, do not collect $200.
Our Python script will execute as we expect it to when we run it on its own. With this setup, we can’t have a shebang at the top, though.
The only really tricky bit is that this method results with an extra “ at the beginning and end of the script’s raw string literal in C++. We have to strip those out manually.
🔗 Give it a Whirl
I am absolutely certain there would be no shortage of bash shell in hell… so here’s how you’d compile and run this code on it.
g++ main.cpp
./a.out
🔗 Let’s Chat
Comments? Questions? Does this post inspire you to write all your comments as loose strings???
I’d love to hear about any related hacks you’re up to!
I started a twitter thread (right below) so we can chat
new blog post!!!
— mmore500 (@mmore500) March 31, 2021
in which I follow @LilNasX str8 into the fiery pits of hell w/ an unholy hack to #include python scripts into C++ source
🙃 <-- me
⬇️
👑 <-- @LilNasX
🔥🔥🔥🔥😈🔥<-- hellhttps://t.co/U9MvAIDzWm