Dynamic function calls
This problem is not too much about standard C++, but it may be interesting
anyway. Sadly, the code given in this problem will only work on 32-bit X86
machines. On Visual Studio, it is quite easy to compile for 32-bit (it is
the default on VS2008 at least), either change it in the IDE, or open the
appropriate Visual Studio Command Prompt. On linux (using gcc at least),
you can simply add the switch -m32 and it will work if you have gcc-multilib
or similar package installed on your system. Anyway, the problem is solvable
without running any code!
This is something I tried to do when implementing dynamic function calls in the Storm compiler. I needed something similar to the reflections in Java, where it is possible to pass an array of parameters to a function handle. So I started coding and came up with the code shown below, and it seemed to work fine. It worked fine until I called functions with 4 parameters, then it crashes. The question is why?
Background
Function calls in C/C++ uses the cdecl calling convention
by default. This means that parameters are passed on the stack, pushed
from right to left. A call to add1(2, 3, 4) would look like this:
push 4; push 3; push 2; call add1; pop ?; pop ?; pop ?;
It is the callers responsibility to clear the stack, that is why we do the three
pop instructions afterwards. Since we do not care about what was popped, we
can simply do add esp, 12 instead.
The stack on x86 grows towards lower addresses (ie push eax is sub esp, 4; mov eax, [esp]),
we can see that the contents of the stack just before the function call is the same order
as the elements would be stored in an array. That means we can call a function with
parameters from an array like this:
1: Copy the stack pointer somewhere (first asm block)
2: Copy the array byte-for-byte to the top of the stack (memcpy)
3: Adjust the stack pointer and call the function (second asm block).
main.cpp
#include <iostream>
#include <cstring>
using namespace std;
typedef size_t nat;
// Call the function 'fn', with a dynamic list of parameters.
nat callFn(void *fn, nat count, nat *params) {
nat pSize = count * sizeof(void *);
nat *stack;
nat result;
#if defined(_M_IX86)
__asm mov stack, esp;
#elif defined(__i386__)
__asm__ ("movl %%esp, %0;\n" : "=r"(stack)::);
#else
#error "NOT SUPPORTED, only works on x86!"
#endif
// Copy parameters to the stack.
nat *to = stack - count;
memcpy(to, params, pSize);
#if defined(_M_IX86)
__asm {
sub esp, pSize;
call fn;
add esp, pSize;
mov result, eax;
};
#elif defined(__i386__)
__asm__ ("subl %1, %%esp;\n"
"call *%2;\n"
"add %1, %%esp;\n"
"mov %%eax, %0;\n"
: "=r"(result)
: "r"(pSize), "r"(fn) : "memory");
#endif
return result;
}
// Simple functions that add some parameters.
nat add1(nat a) { return a; }
nat add2(nat a, nat b) { return a + b; }
nat add3(nat a, nat b, nat c) { return a + b + c; }
nat add4(nat a, nat b, nat c, nat d) { return a + b + c + d; }
nat add5(nat a, nat b, nat c, nat d, nat e) { return a + b + c + d + e; }
// Test it!
int main() {
nat numbers[] = { 1, 2, 3, 4, 5, 6 };
cout << "1: " << callFn((void *)add1, 1, numbers) << endl;
cout << "2: " << callFn((void *)add2, 2, numbers) << endl;
cout << "3: " << callFn((void *)add3, 3, numbers) << endl;
cout << "4: " << callFn((void *)add4, 4, numbers) << endl;
cout << "5: " << callFn((void *)add5, 5, numbers) << endl;
return 0;
}