PDA

View Full Version : C plugins for scar



Yakman
12-17-2008, 06:32 PM
I've been doing some research on writing plugins for scar in the c language, i think its time i publish some findings.
this tutorial assumes reasonable good c knowledge, but if you dont know it you could read it and you might learn something.
previous knowledge of scar plugins written in delphi is useful too, at least reading TestPlugin.dpr

this is mostly a continuation of the C++ Plugins! (http://www.villavu.com/forum/showthread.php?t=13118) thread by BenLand100.

thoughout this thread, the macro SCAR_EXPORT is defined as shown


#define SCAR_EXPORT _declspec(dllexport) __stdcall

it needs to be used on all functions that scar calls

Working with arrays
This is about writing functions that can handle arrays passed from scar.
for example this code


var
a: array of Integer;

a:= [2, 3, 4, 5, 2, 4, 2];
CPluginIntegerArray(a);



now in memory, an array passed from scar looks like this



high [4 bytes]
pointer-to-data [4 bytes]


high is the length of the array minus one.
i suspect that is why the scar function High() is faster then the scar function GetArrayLength(), because the latter needs to add one.

pointer to data is what its name suggests, you can treat it like a c-style array and dereference it.

both these are intel-style words, as in they have the low byte first (opposite of common sense)


the way i handle this is by making a c struct.



typedef struct scar_array {
int high;
void* data;
} scar_array;




void SCAR_EXPORT
c_plugin_integer_array(scar_array thearray) {

int* data = (int*)thearray.data;
int f;
for(f = 0; f <= thearray.high; f++)
//do stuff with data[f]
}


then you just export this function as normal.
"procedure CPluginIntegerArray(a: array of Integer);"

you may notice that 8 bytes must be pushed onto the stack to call this function, to have it slightly more efficent, declare the function as passing var.
"procedure CPluginIntegerArray(var a: array of Integer);"

so scar_array becomes a pointer which you must dereference.
"c_plugin_integer_array(scar_array* thearray) {"


Exporting types
plugins can export types with the functions GetTypeCount() and GetTypeInfo()



int SCAR_EXPORT
GetTypeCount(void) {
//stuff
}

int SCAR_EXPORT
GetTypeInfo(int x, char** type_name, char** type_def) {
//stuff
}


this isnt as easy as exporting functions, because c style strings are not used, you cannot use strcpy() like in GetFunctionInfo().
instead, delphi strings are used, which are much more complicated.

in memory, they look this this


reference-count [4 bytes]
length [4 bytes]
first-char [1 byte] <= the pointer must point to here
second-char [1 byte]
...
...
nth-char [1 byte]
null-terminator [2 bytes]


reference count is something delphi uses for its memory management, its not terribly important here, but if you really want to know, google can explain it reasonable well.

length is pretty obvious i hope. it only includes the length of the string and not the length of the reference count or the two null terminators

you may notice that reference count and length are in negative array indecies.


as before, a c struct is a nice way to handle them


typedef struct delphi_string {
int ref;
int length;
char bytes[1];
} delphi_string;



now GetTypeInfo() can be seen as passing a pointer-to-pointer-to-char.
we actually have to allocate the memory for the string ourself.
I have tried to pass memory allocated statically, or globally, but non worked as i wanted and it seems that heap-allocated memory (with malloc) is a good way.
of course this could cause a memory leak, since you lose the pointer to the memory, but scar holds the memory until it closes, so even if you had the pointer you would never free() it.


in my plugins, i write this function and use it for building up delphi string


/*
* dest - passed by GetTypeInfo()
* src - what the string should contain, regular c string
*/
void
make_delphi_string(char** dest, char* src) {
char* buffer = malloc(256);

*dest = buffer + 8;
delphi_string* str = (delphi_string*)buffer;
str->ref = 255;
str->length = strlen(src);
strcpy(str->bytes, src);
str->bytes[str->length + 1] = 0; //double null
}


and used like this



int SCAR_EXPORT
GetTypeInfo(int x, char** type_name, char** type_def) {

switch(x) {
case 0: {
make_delphi_string(type_name, "TNumber");
make_delphi_string(type_def, "Integer");
} break;
case 1: {
make_delphi_string(type_name, "TNumbers");
make_delphi_string(type_def, "record n1, n2: Integer; end;");
} break;



Calling SCAR Functions
Scar provides a way of calling its functions from a plugin. Although i havent fully understood everything about it, i will post what i know.

scar calls a function from your dll called "SetFunctions" with a parameter that is an array that contains the name and the pointer to the function. The parameter is a delphi array and not the same as the arrays that scar scripts pass.

in memory, this array looks like this


length [4 bytes]
first-function [8 bytes] <= pointer points to here
second-function [8 bytes]
...
...
nth-function [8 bytes]


each function looks like this in memory


pointer-to-c-style-string [4 bytes]
pointer-to-function [4 bytes]


ill make a struct from the function


typedef struct scar_plugin_function {
char* name;
void* ptr;
} scar_plugin_function;


and declare SetFunctions() as shown


SetFunctions(scar_plugin_function* array) {


to find the length we again must look in negative array indicies.


then the idea is you just iterate the array and save the functions you like.


however, there is a problem.
exported scar functions do not use any calling convention that i know of, i have tried stdcall, fastcall and pascal, but non of them work.
until someone works it out, you cannot call functions that take arguments.
functions like GetScarVersion can be used.

so my example is


typedef int (__stdcall *GSV)(void);

void SCAR_EXPORT
SetFunctions(scar_plugin_function* array) {

int length = ((int*)array)[-1];
for(int f = 0; f < length; f++) {
if(strcmp(array[f].name, "GetSCARVersion") == 0) {
GSV fGetScarVersion = (GSV) array[f].ptr;
print("gsv = %d\n", fGetScarVersion());
}
}
}




Build files
you should be able to figure out the .def file yourself, but in case you cant, the one i use is.



EXPORTS
GetFunctionCount = GetFunctionCount@0
GetFunctionInfo = GetFunctionInfo@12
GetTypeCount = GetTypeCount@0
GetTypeInfo = GetTypeInfo@12
SetFunctions = SetFunctions@4



for MinGW, this is the build.bat file i use


@echo off
gcc -c -o plugin.o plugin.c
dllwrap -Xlinker -s -o plugin.dll --def plugin.def plugin.o
pause



C++ plugins
iv written my plugins in c because its slightly cleaner for this kind of work, especially with all the pointer stuff.

but it all translates very easily to c++, except you need to use c-style linkage, because c++ calls functions in a slightly different way, as in it needs to handle exceptions being thrown, and may also push the 'this' pointer. Scar doesnt understand c++ style.



#ifdef __cplusplus
extern "C" {
#endif

int SCAR_EXPORT
GetFunctionCount() {
//....
}

#ifdef __cplusplus
}
#endif

Naum
12-17-2008, 06:46 PM
Wow I actually learned something. Great tutorial Yakman!!

boberman
12-17-2008, 06:53 PM
Yakman, This is AWESOME. Thanks so much for all this info. It really helps.

(goes back to reading again)

lordsaturn
12-17-2008, 09:51 PM
So... C > Delphi?

Yakman
12-17-2008, 10:06 PM
So... C > Delphi?

in general life, yes

but when dealing with scar, delphi > c
however c is gaining, as this tutorial shows.

EDIT:
my lawyers have told me to say that actually delphi is quite good in some circumstances.

Smartzkid
12-17-2008, 10:39 PM
THANK YOU!

This one doesn't like delphi.