GS-Calc (ver. 17 and later) can be configured to use functions written by users in C/C++ as plain DLL libraries. Such functions can be even used as a replacement for the default built-in functions. They can accept and return all the argument types and additionally they can also return images displayed directly in worksheets or messages displayed after each update. What’s important, using these functions GS-Calc retains the complete multicore support, the same speed and memory requirements (e.g. no extra arrays are allocated when passing ranges as arguments). You can add virtually any number of such functions.
Libraries are added by the Settings > Imported Function Libraries command as shown on the screenshot below. The internal function names must match the original C/C++ names. The displayed names are what is entered in worksheet cells and the only requirement is that they must be unique.
The added functions must be declared as
typedef double(__cdecl *DLL_EXT_FUNCTION)(GSCalcArg *arg);
where GSCalcArg is a structure enabling you to pass up to 15 arguments: numbers, strings and ranges/arrays.
struct ArrayItem
{
uint8_t type; // DLL_ARGT_EMPTY | DLL_ARGT_DOUBLE | DLL_ARGT_TEXT
uint8_t err_code; // ERROR1_DIV_BY_ZERO...ERROR1_SYNTAX_ERROR
uint16_t col; // [0, 4095]
uint32_t row; // [0, 12582911]
double num; // numeric cell
char text[MAXINPUT + 1];// text cell; max. 1024+NULL char. utf-8 string
};
typedef int(__cdecl *READ_ARRAY)(void *env, int array_index, ArrayItem *val);
typedef int(__cdecl *WRITE_ARRAY)(void *env, int array_index, ArrayItem *val);
typedef void* (__cdecl *MALLOC)(size_t s);
typedef void(__cdecl *MFREE)(void *_Memory);
struct GSCalcArg
{
uint8_t types[16]; // in: as in ArrayItem.type, out: DLL_ARGT_EMPTY...DLL_ARGT_MESSAGE
uint8_t errors[16]; // ERROR1_DIV_BY_ZERO...ERROR1_SYNTAX_ERROR
double numbers[16];
char *strings[16]; // input (UTF-8) 0...14 strings are preallocated by GS-Calc and must not be overwritten;
// strings[15] is used for output strings and already points to a MAXINPUT+1 char. temp. buffer
struct Dims
{
uint32_t cx;
uint32_t cy;
} array_dims[16]; // dimensions of the subsequent and grouped together range/array arguments passed from GS-Calc
struct
{
BYTE *data; // DIB data if you return types[15] = DLL_ARGT_IMAGE; must be (de-)allocated with memory_alloc/memory_free
size_t size; // DIB data size
Dims dims; // optional resizing when displaying the image (returned either as a DIB or a file path)
MALLOC memory_alloc;
MFREE memory_free;
} image;
READ_ARRAY read_array; // returns ArrayItem.type
WRITE_ARRAY write_array; // returns ArrayItem.type or -1 if the out-of-memory condition occurs
void *env; // internal data that must be passed back in read_array/write_array calls
};
Indices 0…14 in types and errors in GSCalcArg represent all subsequent argument types and errors from left to right passed from GS-Calc.
Indices 0…14 in numbers, strings and array_dims point to argument values after breaking them down into 3 groups (numbers, strings and ranges/arrays) so each group receives its own counter.
On output, the index 15 (DLL_ARGC_RET) is used to specify the returned type, error code and value. The types[15] and errors[15] are always set obligatorily.
The returned values are stored as follows:
- double number - the numbers[15] value must be set accordingly.
- string (null terminated) - must be copied to the strings[15] buffer (up to 1024+1 characters including NULL) preallocated by GS-Calc.
- array - the returned array is filled with the write_array function and array_index set to 15. The array_dims[15] array size is determined automatically based on the col/row values passed to write_array. It can be overwritten if needed.
- image - either BYTE *data and size_t size in Image are set or the full file path is copied to strings[15]
Examples:
// messageIf(condition, text)
//
// arguments:
// condition, message text
// returns:
// if condition != 0, 'messageIf' displays 'text' as a message box after each update/recalculation
// otherwise the message text is displayed in a cell as normal text
__declspec(dllexport) double __cdecl messageIf(GSCalcArg *arg)
{
uint8_t errorCode = 0;
if (arg->types[0] != DLL_ARGT_DOUBLE && arg->types[0] != DLL_ARGT_EMPTY || arg->types[1] != DLL_ARGT_TEXT)
errorCode = ERROR1_INVALID_VALUE;
if (arg->types[2] != DLL_ARGT_NONE) // more than 2 arguments detected
errorCode = ERROR1_SYNTAX_ERROR;
if (!errorCode && arg->errors[0])
errorCode = arg->errors[0];
if (!errorCode && arg->errors[1])
errorCode = arg->errors[1];
if (!errorCode)
{
::strncpy(arg->strings[DLL_ARGC_RET], arg->strings[0], 1024);
arg->strings[DLL_ARGC_RET][1024] = 0;
return arg->types[DLL_ARGC_RET] = (arg->numbers[0] ? DLL_ARGT_MESSAGE : DLL_ARGT_TEXT);
}
else
{
arg->errors[DLL_ARGC_RET] = errorCode;
return arg->types[DLL_ARGC_RET] = DLL_ARGT_EMPTY;
}
}
// filter(range, column, number)
//
// arguments:
// range - a range/array
// column - column index (from 0 to the number of columns in 'range' - 1)
// number - numeric value to perform equality check
// returns:
// an array consisting of rows of 'range' containing 'number' in the specified column
__declspec(dllexport) double __cdecl filter(GSCalcArg *arg)
{
uint8_t errorCode = 0;
if (arg->types[0] != DLL_ARGT_ARRAY || arg->types[1] != DLL_ARGT_DOUBLE || arg->types[2] != DLL_ARGT_DOUBLE)
errorCode = ERROR1_INVALID_VALUE;
if (arg->types[3] != DLL_ARGT_NONE)
errorCode = ERROR1_SYNTAX_ERROR;
if (!errorCode && arg->numbers[0] >= arg->array_dims[0].cx)
errorCode = ERROR1_INVALID_VALUE;
if (!errorCode && arg->errors[0])
errorCode = arg->errors[0];
if (!errorCode && arg->errors[1])
errorCode = arg->errors[1];
if (!errorCode && arg->errors[2])
errorCode = arg->errors[2];
ArrayItem x = { 0 };
int outRow = 0;
for (uint32_t inRow = 0; inRow < arg->array_dims[0].cy && !errorCode; ++inRow)
{
x.row = inRow;
x.col = static_cast<uint16_t>(arg->numbers[0]);
if (arg->read_array(arg->env, 0, &x) == -1)
{
errorCode = x.err_code;
break;
}
bool match = false;
if (x.type == DLL_ARGT_DOUBLE)
match = (x.num == arg->numbers[1] || x.err_code && x.err_code == arg->errors[2]);
else if (x.type == DLL_ARGT_EMPTY)
match = (arg->numbers[1] == 0);
else
errorCode = ERROR1_INVALID_VALUE;
if (match)
{
for (x.col = 0; x.col < arg->array_dims[0].cx && !errorCode; ++x.col)
{
x.row = inRow;
arg->read_array(arg->env, 0, &x);
x.row = outRow;
// for best performance when writing to very large arrays, try to write in the left-to-right and top-to-bottom order
if (arg->write_array(arg->env, DLL_ARGC_RET, &x) == -1) // -1 means the out-of-memory condition or an invalid row/column
errorCode = x.err_code;
}
++outRow;
}
}
arg->errors[DLL_ARGC_RET] = (!outRow ? ERROR1_NULL_VALUE : 0);
return arg->types[DLL_ARGC_RET] = DLL_ARGT_ARRAY;
}