This project is motivated by recently writing an Emacs module. Writing an Emacs module requires an amount of boilerplate around the functions in C/C++ that you would like to call, which this library intends to automate the generation of. The boilerplate, which is mostly unpacking and verifying arguments provided by the ELISP caller, is amenable to being generated by a macro. For a good discussion of writing Emacs modules, see http://diobla.info/blog-archive/modules-tut.html#sec-2-2. The goal of this library is to turn something like this:
/** This is some function that encapsulates the work you need done in C++ */
emacs_value some_C_work_useful_for_Emacs(int a, const string& s, void* data) {
}
/* This is the function that actually gets called from and is registered with Emacs */
emacs_value elisp_callable(emacs_env* env, ptrdiff_t nargs, emacs_value* args, void* data) {
// Assert nargs contains 1 or 2
// unpack an int from args with error handling
// check if nargs == 1 or 2, and unpack a string if it is 2 with error handling
return some_C_work_useful_for_Emacs(i, s, data);
}
/* This is how modules are initialized, and how functions are registered for being called from elisp */
int emacs_module_init(struct emacs_runtime *runtime) noexcept {
emacs_env* env = runtime->get_environment(runtime);
emacs_value func = env->make_function(env,
1, // Minimum parameter count
2, // Maximum parameter count
elisp_callable,
"Callable function from emacs",
nullptr); // User data passed to your function
emacs_value symbol = env->intern(env, "lisp-function-name");
emacs_value args[] = { symbol, func };
emacs_value defalias = env->intern(env, "defalias");
env->funcall(env, defalias, 2, args);
}
into something like this:
/* This is your same function that encapsulates work you need to do in C++ */
emacs_value some_C_work_useful_for_Emacs(emacs_env* env, const string s, std::optional<int> i) {
cout << s << endl;
cout << i.value() << endl;
return env->intern(env, "nil");
}
EmacsCallable<some_C_work_useful_for_Emacs> c;
int emacs_module_init(struct emacs_runtime *runtime) noexcept {
c.defineInEmacs(runtime, "emwt-lisp-callable", "Test function", nullptr,
elispCallableFunction<&c>);
return 0;
}
In other words elisp_callable
and emacs_module_init
are automatically generated.
elispCallableFunction
takes a functor address as a non-type template parameter, and when it is invoked, it invokes the functor. The functor is generated by the function you provide, and it will unpack arguments and call your function.
This chart shows a run in Instruments, collecting "Cycles with outstanding L1 misses", "DTLB misses that incur a page walk", and "Total Cycles". I used the Signposts API to limit to time spent in user-function invocation (i.e. the wrapper function that calls the user function provided by the module). There is a flaw (or, at least one) in how I collected the numbers, which is that I do not know how to limit perf counter metrics to just time spent between signposts. I think it requires a custom instrument or more advanced collection. Therefore, I had to analyze perf counter data from the first sign post to the last one, which includes time the process is context switched out.
Some future optimizations planned are:
- Use a statically-allocated buffer for strings to reduce the need to allocate heap memory during the function invocation.
- When unpacking parameters, figure out a way to unpack them directly into the arguments tuple (similar to emplace functions), instead of returning them and relying on copies being made.
Instruments screen shot