Before continuing reading, consider that there are 2 different issues being discussed at the same time.
- One is whether one function per event should be used or whether one case label per event should be used.
- The other is how the arguments to the event should be passed.
Function or HandleEvent?
The problem with functions as opposed to a HandleMessage function is exaggerated big time in this thread.
More specifically, with a C ABI, functions have, AFAICS, no disadvantages for the interface compared to a single HandleEvent function.
Lets summarize the options in code snippets: (parameter passing left out cause it's different subject)
HandleMessage:
Code: Select all
void HandleEvent(void* ai, int id, /* ... */) {
switch (id) {
case ENEMY_ENTERED_LOS_EVENT:
// ...
break;
case ENEMY_LEFT_LOS_EVENT:
// ...
break;
// ...
}
}
Functions:
Code: Select all
void EnemyEnteredLos(void* ai, int id, /* ... */) {
// ...
}
void EnemyLeftLos(void* ai, int id, /* ... */) {
// ...
}
The entire problem with the current interface is that if a single function is added, the AI must be recompiled,
because the interface classes' vtable changes, and the vtable's format is pretty much undefined (read no standard ABI). With C ABI, there is no undefined-format vtable, so this problem goes away magically.
With simple C functions however, it is trivial to put a pattern in spring code like this, so events are entirely opt-in from the AI side, just like with HandleEvent approach:
Code: Select all
// on loading the AI:
EnemyEnteredLosEvent = (ENEMYENTEREDLOSEVENT)GetFunctionPointer(ai_dll_handle, "EnemyEnteredLosEvent);
// when calling the event in the engine:
if (EnemyEnteredLosEvent != NULL) // only call event if AI handles it
EnemyEnteredLosEvent(...);
(left parameter passing details away for brevity)
Note also how similar this is to the standard pattern to call events in .NET:
Code: Select all
public event EventHandler<EventArgs> EnemyEnteredLos;
void OnEnemyEnteredLos(EventArgs e) {
if (EnemyEnteredLos != null)
EnemyEnteredLos(this, e);
}
This is a good thing IMHO because people who have done .NET development will immediately recognize the pattern.
The only minor disadvantage of functions I do see however, is that it is slightly more code on the engine side.
With regards to binary compatibility, about which this thread seems to be mostly, adding an exported function to an AI does not break binary compat, so a newer AI can always be used in an older Spring version. (forward compatibility!)
Adding an extra event does also not break binary compatibility (as long as the engine doesn't unconditionally call it, ie. the conditional in above code sample must be present), so an older AI can always be used in a newer Spring version. (backward compatibility!)
tl;dr: The choice between HandleEvent approach and multiple functions approach when doing a C interface (NOT C++) is arbitrary, and neither of the approaches has significant advantages over the other.
Parameter passing
Parameter passing isn't problematic in any way when we disregard compatibility. However, when we introduce it does become a problem, as you want to prevent breaking interfaces all the time by changing parameters passed to an event.
For brevity, I will assume that any other modification on a parameter list apart from adding a new parameter on the end will break binary compatibility for this event. Therefore such modifications do not have to be considered. (There are ways to attack them but it's outside scope of this post.)
The two options for parameter passing then are:
- passing them as regular function arguments, and
- passing a pointer to a structure/union containing the arguments.
The choice here is much easier. Passing parameters as regular function arguments is already impossible if you use HandleEvent approach in first part of post (read earlier posts about evilness of varargs).
When using functions, parameter passing is ok-ish until you start adding arguments. Only with some calling conventions one can safely add extra arguments to an argument list without breaking compat (the ones that let caller pop stack (as opposed to callee)). Because this gets way too technical, it's a bad solution
The other options is passing a pointer to a structure. Because structures/unions are always laid out identically in memory, provided you set right alignment setting, they provide a much stronger basis for ABI compatibility.
So here the choice is easy ... use structures for passing arguments if you ever want to add new arguments to existing events,
even if you use regular functions to handle the events!
Only downside of structures is the setup/teardown anti pattern thats needed engine side to call the events (well without teardown usually, I assume). With some smart #ifdef..#endif'ing constructors in them this can be overcome easily though:
Code: Select all
struct EnemyEnteredLosEventArgs {
#ifdef __cplusplus
EnemyEnteredLosEventArgs(int unit_id) : unit_id(unit_id) {}
#endif
int unit_id;
};
// this Spring internal function calls the AI-side event, whether it's HandleEvent() or EnemyEnteredLos()
OnEnemyEnteredLos(EnemyEnteredLosEventArgs(unit_id));
tl;dr: use structures to pass parameters
Hope this clears some stuff up
