Suppose I have a service that controls lights. It's attached to a common struct interface, like this:
typedef struct light_controller light_controller_t;
struct light_controller {
void (*switch)(light_controller_t *self, bool on);
}
I use it like this:
light_controller_t *lights = ...
lights->switch(lights, false);
And it's implemented like this:
static void lights_switch_impl(light_controller_t *self, bool on) {
self->room->lit = on;
}
...
void lights_controller_default_init(lights_controller_t *self) {
self->switch = lights_switch_impl;
}
Neat. Okay. Function pointers in C are powerful stuff -- powerful enough that C++ was originally a preprocessor, not a language unto itself.
Here's the thing I want to do. Imagine lights
is part of a hierarchy. We have a light_controller_t
for a whole building, and one for every room. We manage this hierarchy separately. When we turn off a parent light controller, we want all the child lights to be explicitly switched off.
This is easy. We implement the switch
function to do its own work, and call the function for any child.
static void lights_switch_impl(light_controller_t *self, bool on) {
self->room->lit = on;
for (light_controller_t *child = self->child; child; child = child->next) {
child->switch(child, on);
}
}
Suppose then that we implement a lot of services, controlling windows, radios, ovens, etc. There are a lot of function calls here. Some parts of the hierarchy won't have some services (e.g. first_floor.services[oven_controller]
) but will have children that do (i.e. first_floor.children[kitchen].services[oven_controller]
).
You can think of this as a tree of composed service nodes. Each node in the tree is a service that may or may not implement certain interfaces. The little services might be pretty simple.
But what do we do when a node doesn't implement a service? We want child nodes implementing the service to receive the invocation. Something like this (pseudocode):
function service_node.get_service(service_type):
if (self implements service_type)
return self as service_type
else
return service_proxy(self, service_type) as service_type
end
end
function service_proxy(parent, service_type):
self = new service_type()
for (method in service_type.methods)
method = function(args...):
for (child in parent.children)
child.method(args)
end
end
end
return self
end
Now, if you know something about C, you know that there isn't any easy way to do this. For starters, the language features aren't present: we've got a closure we can work around, but we've also got something that looks like varargs but can't be, because varargs in C are a dead end that can't generically "splat" back to arguments.
"But Justin," you say (startling me), "this is a clear case of using the wrong language for the task." And that is certainly true for the task I have described. Rejoice, Rubyists (or "Rubes," as they are popularly known):
class Proxy
def initialize(owner)
@owner = owner
end
private
def method_missing(method, *args, &block)
@owner.children.each { |child|
child.send(method, *args, &block)
}
end
end
I know. I know! Awesome! (Other languages can do this too; for example, PHP has __call
, which does the same job but provides 96% less smug satisfaction).
The problem is that we're dealing with an isolated hypothetical, and while Ruby may or may not be 67 times slower than C, it sure isn't faster. The real-life version of the problem I'm describing has to do this resolution thousands of times per second at a speed that won't be more than a blip in profiling, or I'll have to rip it out. Dynamic Language Du Jour isn't the answer. I only need tiny subset of dynamic language features for some limited application, providing all the speed that comes from that lack of flexibility. An abomination is only abominable in proportion to its uselessness!
(As a digression, I believe the previous sentence should be in the foreword of any book about C++.)
But Ruby's original interpreter, MRI, is written in C. Everything is possible in C + inline asm
for a sufficiently painful definition of "possible," right? Isn't this all just a bunch of registers, some instructions, a heap and a stack? All we want to do (I think) is forward an invocation, exactly as-is, to an address we provide at runtime. We can refresh our memory of calling convention and patch up a generic call ourselves using pointer math. No?
function service_proxy(parent, service_desc):
proxy_method = pseudo_asm(self, args):
args = stack_pop() // take args
self = stack_pop() // take self
for (child in parent.children)
stack_push(args) // copy args
stack_push(child) // self pointer will be child instead
next_eip = (eip - self) + child // uh, maybe something like this
call next_eip // magic! :)
// ...wait, who cleans up the stack again?
end
end
for (method in service_desc.methods)
method = proxy_method
end
return self
end
No.
Obviously this only looks easy because this pseudocode pretends it's O.G., using terms like eip
, while it totally glosses things like the for
each loop, and how it knows sizeof(args)
, and its recent One Direction collaboration. We could leave extra stack space -- say, enough for ten doubles. This doesn't look impossible.
Crap, the function might return a value. How much do we copy back?
Uhh.
Unnng.
Stone age computing hurt brain!
Let's JFGI.
Here's an actual i386/x86_64 implementation (condensed), courtesy of StackOverflow user Coltox, who looks like he signed up for the express purpose of ninja-ing this answer at all the people who said it couldn't be done. It can be done, and to answer the implicit question, it's very painful.
The code that follows splats one varargs call to one function invocation, the signature of which is available at compile time.
#include <limits.h>
#include <stdint.h>
#include <alloca.h>
#include <inttypes.h>
#include <string.h>
/* Currently we don't care about floating point arguments and
* we assume that the standard calling conventions are used.
*
* The wrapper function has to start with VA_WRAP_PROLOGUE()
* and the original function can be called by
* VA_WRAP_CALL(function, ret), whereas the return value will
* be stored in ret. The caller has to provide ret
* even if the original function was returning void.
*/
#define __VA_WRAP_CALL_FUNC __attribute__ ((noinline))
#define VA_WRAP_CALL_COMMON() \
uintptr_t va_wrap_this_bp,va_wrap_old_bp; \
va_wrap_this_bp = va_wrap_get_bp(); \
va_wrap_old_bp = *(uintptr_t *) va_wrap_this_bp; \
va_wrap_this_bp += 2 * sizeof(uintptr_t); \
size_t volatile va_wrap_size = va_wrap_old_bp - va_wrap_this_bp; \
uintptr_t *va_wrap_stack = alloca(va_wrap_size); \
memcpy((void *) va_wrap_stack, \
(void *)(va_wrap_this_bp), va_wrap_size);
#if ( __WORDSIZE == 64 )
/* System V AMD64 AB calling convention */
static inline uintptr_t __attribute__((always_inline)) va_wrap_get_bp()
{
uintptr_t ret;
asm volatile ("mov %%rbp, %0":"=r"(ret));
return ret;
}
#define VA_WRAP_PROLOGUE() \
uintptr_t va_wrap_ret; \
uintptr_t va_wrap_saved_args[7]; \
asm volatile ( \
"mov %%rsi, (%%rax)\n\t" \
"mov %%rdi, 0x8(%%rax)\n\t" \
"mov %%rdx, 0x10(%%rax)\n\t" \
"mov %%rcx, 0x18(%%rax)\n\t" \
"mov %%r8, 0x20(%%rax)\n\t" \
"mov %%r9, 0x28(%%rax)\n\t" \
: \
:"a"(va_wrap_saved_args) \
);
#define VA_WRAP_CALL(func, ret) \
VA_WRAP_CALL_COMMON(); \
va_wrap_saved_args[6] = (uintptr_t)va_wrap_stack; \
asm volatile ( \
"mov (%%rax), %%rsi \n\t" \
"mov 0x8(%%rax), %%rdi \n\t" \
"mov 0x10(%%rax), %%rdx \n\t" \
"mov 0x18(%%rax), %%rcx \n\t" \
"mov 0x20(%%rax), %%r8 \n\t" \
"mov 0x28(%%rax), %%r9 \n\t" \
"mov $0, %%rax \n\t" \
"call *%%rbx \n\t" \
: "=a" (va_wrap_ret) \
: "b" (func), "a" (va_wrap_saved_args) \
: "%rcx", "%rdx", \
"%rsi", "%rdi", "%r8", "%r9", \
"%r10", "%r11", "%r12", "%r14", \
"%r15" \
); \
ret = (typeof(ret)) va_wrap_ret;
#else
/* x86 stdcall */
static inline uintptr_t __attribute__((always_inline)) va_wrap_get_bp()
{
uintptr_t ret;
asm volatile ("mov %%ebp, %0":"=a"(ret));
return ret;
}
#define VA_WRAP_PROLOGUE() uintptr_t va_wrap_ret;
#define VA_WRAP_CALL(func, ret) \
VA_WRAP_CALL_COMMON(); \
asm volatile ( \
"mov %2, %%esp \n\t" \
"call *%1 \n\t" \
: "=a"(va_wrap_ret) \
: "r" (func), \
"r"(va_wrap_stack) \
: "%ebx", "%ecx", "%edx" \
); \
ret = (typeof(ret))va_wrap_ret;
#endif
Uhng. How it work?
int __VA_WRAP_CALL_FUNC proxy_printf(char *str, ...)
{
VA_WRAP_PROLOGUE();
int ret;
VA_WRAP_CALL(printf, ret);
printf("printf returned with %d \n", ret);
return ret;
}
Ladies and Gentlemen...printf
.
(Bill: "I know, let's make a proxy to printf
the return code from printf
!" Carol: "Go home Bill, you're drunk.")
Still! Now we have a working solution! Let's hook it up!
...except that floats get passed in special float registers: don't pass any floats. Also, it doesn't do __cdecl
or __fastcall
. Also, it doesn't do ARM or PPC. Also, THIS IS MORE WORK. Remember our "manual" proxy?
static void lights_switch_impl(light_controller_t *self, bool on) {
self->room->lit = on;
for (light_controller_t *child = self->child; child; child = child->next) {
child->switch(child, on);
}
}
Looks pretty good right now, doesn't it?