Compile a string into a callable object in C++: a simple JIT based on system, dlopen and dlsym Today I discovered that it is possible to dynamically load code at runtime via dlopen and dlsym (and similar utilities on Windows as well). So the obvious next step was to call the compiler through system to build a piece of code into a shared object, which is then loaded to obtain a function pointer. All wrapped in a template class which becomes callable once &quot;compiled&quot;.
The code is pretty much at an embryo state (less than 100 lines including the example), but it works and shows the idea, therefore I would love to hear your opinion.
This thing looks sooo simple and yet sooo powerful that I would really be surprised to be the first one having thought about it.
// compile with something like: g++ -std=c++20 jit.cpp -ldl

#include &lt;dlfcn.h&gt;
#include &lt;cstdlib&gt;
#include &lt;iostream&gt;
#include &lt;filesystem&gt;

template&lt; class &gt;
class JitFunc;

template&lt; class R, class... Args &gt;
class JitFunc&lt;R(Args...)&gt; {
  public:
  using Func = R(Args...);
  using result_type = R;
  
  private:
  std::string soName;
  
  void * dlso = nullptr;
  Func * func = nullptr;

  public:
  // remember to call compile() anytime you modify these strings
  std::string name;                 // the function name, which should also be found in the source
  std::string source;               // the source code, remember to use extern &quot;C&quot;
  std::string cxx = &quot;/usr/bin/g++&quot;; // the compiler executable
  std::string soPath = &quot;/tmp&quot;;      // where to store the shared object (ideally a ramdisk)
  std::string flags;                // custom compiler flags which you may need

  JitFunc(std::string funcName): 
    name(std::move(funcName))
  {}

  ~JitFunc() { 
    close();
    removeSo();
  }

  result_type operator()(Args &amp;&amp; ... args) const {
    return func(std::forward&lt;Args&gt;(args)...);
  }
  
  void compile() {
    close();
    removeSo();

    soName = soPath + &quot;/jit_&quot; + name + &quot;.so&quot;;
    std::string cmd = cxx;
    auto push = [&amp;cmd](std::string s) { cmd += &quot; &quot;; cmd += std::move(s); };
    push(flags);
    push(&quot;-shared -fPIC -o&quot;);
    push(soName);
    push(&quot;-xc++ - &lt;&lt; EOF\n&quot;);
    push(source);
    push(&quot;\nEOF\n&quot;);

    std::system(cmd.c_str());

    dlso = dlopen(soName.c_str(), RTLD_NOW);
    if (!dlso) throw std::runtime_error(dlerror());
    func = (Func*) dlsym(dlso, name.c_str());
    if (!func) throw std::runtime_error(dlerror());
  }

  void close() {
    if (dlso) {
      dlclose(dlso);
      dlso = nullptr;
      func = nullptr;
    }
  }

  void removeSo() {
    if (!soName.empty()) {
      std::filesystem::remove(soName);
      soName.clear();
    }
  }

};

int main() {
  JitFunc&lt;int(int)&gt; f(&quot;timestwo&quot;);
  f.source = 
R&quot;CODE(
#include &lt;iostream&gt;
extern &quot;C&quot; {
int timestwo(int a) { std::cout &lt;&lt; &quot;hello from timestwo&quot; &lt;&lt; std::endl; return 2*a; }
}
)CODE&quot;; // using a string literal to avoid having to escape all the double quotes

  f.compile();
  std::cout &lt;&lt; f(2) &lt;&lt; std::endl;

  return 0;
}
Make better use of constructors
Make your constructor take the program source as well as the function name as arguments, and have it compile the code at construction time. This avoids having to manually call compile(), which avoids errors such as forgetting to call that function or calling it twice.
The destructor already calls close() and removeSo(). You should make the latter two functions private, so one cannot accidentily call them manually.
Avoid unnecessary member variables
You should only declare member variables to hold information that you need for a longer period of time. Variables like source, cxx and so on are only used during the compilation step, they are not needed for anything else. Just remove them, and pass them as parameters to compile(), or better to the constructor, as mentioned above.
Avoid using &lt;&lt;EOF with user-provided content
There's a potential issue: what if I do the following?
f.source = R&quot;CODE(
extern &quot;C&quot; {
int timestwo(int a) {
    return 2 * a;
}
}
EOF
sudo rm -rf /
)CODE&quot;;

It's better to write the source code to a temporary file, and point the compiler to that temporary file. This way, you won't accidentily execute shell code.
Use cases for JIT compilation
This looks very powerful, but what use cases do you have in mind for it? The example you gave is a bad use case; you could just have written timestwo() directly in the program. The main use case I can think of is to provide some way to evalute expressions provided by a user at runtime. In that case, you really want to sanitize the input. Because even if you avoid the shell exploits mentioned above, what happens if you do:
f.source = R&quot;CODE(
extern &quot;C&quot; {
int timestwo(int a) {
    system(&quot;sudo rm -rf /&quot;);
    return 2 * a;
}
}
)CODE&quot;;

If the code you compile is in any way provided by the user, you have to be extremely careful to avoid potential misuse. So I would only use this if I am absolutely sure I need this functionality and if I can make sure it cannot be abused.