I'm working on a project which needs to schedule functions to be executed in a specific thread(s) (possibly in another process as well). The specific reasons why are various.
Assume -
At first, I was needing to schedule less than a dozen or so functions. There was simply a container with an integer-type function identifier and a method of storing the function parameters (byte array, linked list... it didn't matter at first). We used a switch statement to identify which function to execute, and each case also assumes the number of parameters have been stored.
As the project progressed, the number of functions to be scheduled has grown by several dozen, to the current count of about 110. While that in and of itself wasn't a problem, keeping the enumeration for which function belonged to which case did become a problem to some of our newer guys. Additionally, as the project progressed, it became a concern that the code size for something so simple was increasing a great deal as the number of functions and parameters was increasing.
Unfortunately, none of the other programmers on the project could come up with a better solution.
After some thought, I a prototypeless function pointer occurred to me:
This does solve the issue of the enumeration growing, as you'd simply call the passed function pointer rather than doing a switch. Ironically, this should also save a little bit of CPU, although it's probably negligible.
It does, however, present a unique situation. The different functions to be pointed to have different numbers of parameters which cannot be identified using a prototypeless function pointer like this one. As far as I am aware, while C/C++ does provide a method of manually pushing any number of parameters onto this function, it does not provide the means to programmatically determine the number of parameters a function has or their boundaries (you can't use a loop to push the parameters). Due to this, it would re-introduce the switch statement to select the number of parameters and call the function as appropriate with that number of parameters.
As such, an Asm hack seemed required. Portability issues are a non-concern, so I proceeded.
Here's what I've come up with. The other programmers on the project have no experience with assembly (and very little experience with functions prototyped with (...)), which makes them unable to check my work. As such, what do you all think of this code? Are there any issues you can see which I may have not foreseen? Anything I may have missed?
I have a static byte array in carrier to hold the parameters simply because I don't want to make two allocations for the test -- one for the carrier structure and another for the parameters. This can obviously result in a buffer overflow of which I am aware. I am also aware it can result in large amounts of memory waste when pointing to functions with few or no parameters when there's dozens of entries in the schedule queue. Assuming no drastic problems with this code, both issues will be reviewed.
Things I will need to do is move carrier to a struct (I don't even remember why I made it a union) which includes the paramsize rather than passing paramsize alongside carrier. Additionally, carrier may eventually be passed to callit() as a pointer rather than inline parameter.
Sample test code below --
[Kp edit: deleted long comment that broke table layout.]
Assume -
- The thread in which the functions are to be executed knows about the functions and is expecting them
- The functions are to be executed in FIFO order - that is, first come/first serve
- The functions do not have the same types of parameters, nay even the same number of parameters
- Obviously due to the function being called in a different thread, the parameters are not currently stored on the stack. As such, the called function assumes ownership of the parameters and will deallocate them as necessary.
- Return values may be ignored.
- Memory use is of little concern (don't waste it obviously)
- CPU use needs to be tight
At first, I was needing to schedule less than a dozen or so functions. There was simply a container with an integer-type function identifier and a method of storing the function parameters (byte array, linked list... it didn't matter at first). We used a switch statement to identify which function to execute, and each case also assumes the number of parameters have been stored.
As the project progressed, the number of functions to be scheduled has grown by several dozen, to the current count of about 110. While that in and of itself wasn't a problem, keeping the enumeration for which function belonged to which case did become a problem to some of our newer guys. Additionally, as the project progressed, it became a concern that the code size for something so simple was increasing a great deal as the number of functions and parameters was increasing.
Unfortunately, none of the other programmers on the project could come up with a better solution.
After some thought, I a prototypeless function pointer occurred to me:
Code Select
typedef void* (__cdecl FUNCPTR)(...);
This does solve the issue of the enumeration growing, as you'd simply call the passed function pointer rather than doing a switch. Ironically, this should also save a little bit of CPU, although it's probably negligible.
It does, however, present a unique situation. The different functions to be pointed to have different numbers of parameters which cannot be identified using a prototypeless function pointer like this one. As far as I am aware, while C/C++ does provide a method of manually pushing any number of parameters onto this function, it does not provide the means to programmatically determine the number of parameters a function has or their boundaries (you can't use a loop to push the parameters). Due to this, it would re-introduce the switch statement to select the number of parameters and call the function as appropriate with that number of parameters.
As such, an Asm hack seemed required. Portability issues are a non-concern, so I proceeded.
Here's what I've come up with. The other programmers on the project have no experience with assembly (and very little experience with functions prototyped with (...)), which makes them unable to check my work. As such, what do you all think of this code? Are there any issues you can see which I may have not foreseen? Anything I may have missed?
I have a static byte array in carrier to hold the parameters simply because I don't want to make two allocations for the test -- one for the carrier structure and another for the parameters. This can obviously result in a buffer overflow of which I am aware. I am also aware it can result in large amounts of memory waste when pointing to functions with few or no parameters when there's dozens of entries in the schedule queue. Assuming no drastic problems with this code, both issues will be reviewed.
Things I will need to do is move carrier to a struct (I don't even remember why I made it a union) which includes the paramsize rather than passing paramsize alongside carrier. Additionally, carrier may eventually be passed to callit() as a pointer rather than inline parameter.
Sample test code below --
Code Select
typedef void* (__cdecl FUNCPTR)(...);
union carrier {
FUNCPTR *funcptr;
char va_test[512];
};
void __cdecl callit(int size, carrier omg);
int __cdecl something(char *buf, int a, int b, int c, int d, int e, int f) {
sprintf(buf, "a = %.x\nb = %.x\nc = %.x\nd = %.x\ne = %.x\nf = %.x\nwtf=%.x\n", a, b, c, d, e, f, a ^ b ^ c ^ d ^ e ^ f);
printf("%s", buf);
return a ^ b ^ c ^ d ^ e ^ f;
}
__declspec(naked) void __cdecl callit(int paramsize, carrier omg) {
__asm {
push ebp;
mov ebp, esp;
push esi;
mov ecx, paramsize;
lea esi, omg;
add esi, SIZE omg.funcptr; // skip the function ptr
shr ecx, 2; // modulo 4 ... this assumes int is (and thus params are in a multiple of) 32-bit
jz _asmparamspushed;
// TODO: look into using repeat opcodes
_asmpushparams:
push [esi];
add esi, 4;
dec ecx;
jnz _asmpushparams;
_asmparamspushed:
call omg.funcptr;
lea esp, [ebp-4];
pop esi;
pop ebp;
retn;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
char buf[256];
carrier omg;
int a, b, c, d, e, f, r;
a = 0x12345678;
b = 0x87654321;
c = 0x12873465;
d = 0x13572468;
e = 0x24687531;
f = 0x18273645;
r = 0;
omg.funcptr = (FUNCPTR *)something;
va_list list, list2;
va_start(list2, omg.funcptr);
va_start(list, omg.funcptr);
va_arg(list, int) = f;
va_arg(list, int) = d;
va_arg(list, int) = e;
va_arg(list, int) = c;
va_arg(list, int) = b;
va_arg(list, int) = a;
va_arg(list, char *) = buf;
callit((int)(list - list2), omg);
printf("--- double check ---\n");
printf("%s", buf);
// let the test display to the console before exiting
gets_s(buf, sizeof(buf));
return 0;
}
[Kp edit: deleted long comment that broke table layout.]