The Crow C API is the backbone that supports the crow programming language. Most if not all functions you will use in the standard library are using the C API in order to speed up execution and reduce code and memory size. This document will explain how the internal C API works, and how you can extend Crow by exposing your own functions.
Before we start talking about functions, lets discuss values. Values are pieces of data that Crow uses. Values are defined as a struct called CRO_Value
. CRO_Value
has multiple fields, however you should pay close attention to two of the fields, type
and value
. type
details what data will be stored in the value, and value
is a union of the actual data the Value is holding onto. There are a few different accessors for value
, for example value.string
, value.function
, value.number
. The full list of accessors can be found in types.h
under the CRO_innerValue
union. The full list of data found in the CRO_Value
can likewise be found in types.h
as well.
To use a value in C, simply define it. CRO_Value
is always passed by value, so there is almost never a need to allocate memory to it (unless of course it is an array, in which case you must allocate memory to it). A simple C program using CRO_Value
could look like:
CRO_Value ourValue;
ourValue.type = CRO_Number;
ourValue.value.number = 6.5;
You are also able to use function-like macros to instantly define certain values with recommended default values where data is unused or not explicitly set. For example, the above code could be replaced with:
CRO_Value ourValue;
CRO_toNumber(ourValue, 6.5);
This will set ourValue
to 6.5
. Some macros may take in different things, for example CRO_toString
will take in the CRO_State
to ensure the string is registered with the garbage collector. The CRO_State
will be discussed later, however the initializer functions macros are as follows:
CRO_toNumber([Value Variable], [Numeric Value])
- Sets theCRO_Value
to a NumberCRO_toNone([Value Variable])
- Sets theCRO_Value
to UndefinedCRO_toBoolean([Value Variable], [Boolean Value])
- Sets theCRO_Value
to a BooleanCRO_toString([CRO_State], [Value Variable], [String])
- Sets theCRO_Value
to a string, make sure the string is able to be freed, i.e. it is not constant. Passing a string to Crow via this way will pass all control over its lifetime to the Garbage Collector.CRO_toPointerType([Value Variable], [Type], [Pointer type])
- Sets theCRO_Value
to any type you choose, although it needs to be a pointer. This can be used to allocate a string without giving the garbage collector control of it. In the future you will be able to give the garbage collector control of your pointer too, but for now that is not possible.
Within Crow, there are three different classes of function: CRO_Function
, CRO_LocalFunction
, and CRO_PrimitiveFunction
. The difference mainly boils down to how the functions are executed or how variables are treated.
A CRO_LocalFunction
is the most easily understood function type, it is simply a function defined in Crow using the func
or defun
commands. For example a lambda passed to a function would be of type CRO_LocalFunction
.
A CRO_Function
is a C function exposed to Crow. These are the functions you will most likely be writing using the C API. This comprises most of the built in functions, such as println
. If one of these is passed in place of a lambda function, it will be of type CRO_Function
, so make sure to account for this if you expect a lambda. For example, each
takes in a lambda, and the lambda is type checked for either CRO_Function
or CRO_LocalFunction
since both work nearly identically, this allows you to use println
with each, it is recommended that if you expect a lambda, you check for both CRO_Function
and CRO_LocalFunction
.
Finally, the strangest of all, CRO_PrimitiveFunction
. A long time ago, all functions were CRO_PrimitiveFunction
s, however as Crow has evolved, this function type has proven to be too unwieldly for general application, and too complicated for simple tasks. A CRO_PrimitiveFunction
is a C function, however all of the arguments passed to it are passed unevaluated. That is, they are passed as char*
, not as CRO_Value
. This is useful in functions like defvar
, where the first argument is the variable name, however for functions like add
for example, it is much easier (on both the programmer and compiler) to simply evaluate the arguments ahead of the function's execution, therefore a CRO_Function
should be used. More detail will be given when we talk about functions in code.
For this, lets look at some real world examples from the Crow codebase.
This is CRO_rand
, it takes zero arguments and returns a random number:
CRO_Value CRO_rand (CRO_State *s, int argc, CRO_Value *argv) {
CRO_Value ret;
CRO_toNumber(ret, (double)rand() / RAND_MAX);
return ret;
}
To begin, notice the function definition, CRO_Value CRO_rand (CRO_State *s, int argc, CRO_Value *argv)
. EVERY single CRO_Function
requires this same definition, or else the compiler will not know what to do with it. It requires this definition whether or not it actually uses arguments. Notice, argc
and argv
are not called at all within CRO_rand
.
Next, notice the CRO_Value ret
. In Crow, every function must return a value. If it does not, it is considered undefined behavior. In this case we want to return the value we randomly generate, so we define the ret
at the top of the file. Next look at how we set the value, using one of the function like macros we used before. You can see here we are setting ret
to rand() / RAND_MAX
in order to get a random number between 0 and 1. Finally after that, we simply return the value.
Now lets look at one thats a little more involved, that is to say, it uses lambdas. This is CRO_doTimes
:
CRO_Value CRO_doTimes (CRO_State *s, int argc, CRO_Value *argv) {
CRO_Value v;
if (argc == 2) {
CRO_Value func, times;
func = argv[1];
times = argv[2];
if (func.type == CRO_Function || func.type == CRO_LocalFunction) {
if (times.type == CRO_Number) {
int i, timesToCall;
timesToCall = times.value.number;
for (i = 0; i < timesToCall; i++)
v = CRO_callFunction(s, func, 0, NULL, 0, func, 0);
}
else {
CRO_toNone(v);
/* Error */
}
}
else {
CRO_toNone(v);
/* TODO: Error */
}
}
else {
char *err;
err = malloc(128 * sizeof(char));
sprintf(err, "[%s] Expected 2 arguments. (%d given)", argv[0].value.string, argc);
v = CRO_error(s, err);
return v;
}
return v;
}
First of all, notice the same function definition as CRO_rand
, despite CRO_doTimes
taking arguments. We can see evidence of it taking arguments in the if (argc == 2)
, this checks if two arguments are supplied. do-times
expects two arguments, the first one being a function, and the second one being a number. argc
holds the number of arguments supplied, and argv
holds their values. In fact argc
means "argument count", and argv
means "argument value".
Next, notice the type checking. We set argv[1]
which we assume to be our function to the variable name func
, and argv[2]
which we assume to be the number of times to execute to times
. Next func.type
is checked. Since func.type
is a lambda, we check if it is a CRO_Function
OR a CRO_LocalFunction
. If you are ever making a function that takes in a lambda, follow this example.
If both func.type
and times.type
check out to their expected types, we run the code, particularly, we turn times into an integer, called timesToCall
, and we call func
that many times. Notice however how we call func
, we use the, admittedly very horrible ane deprot-y, CRO_CallFunction
. Originally CRO_CallFunction
was only used internally, so it takes a lot of arguments which are completely useless to us here, if you ever need to call a lambda or function, simply follow this example as a guide, you will never need to set any of the other arguments.
A side not on error reporting. Error reporting in Crow right now is awful. Its in a half working state, with a lot of legacy errors that need to be updated. I would ask that you follow the bottom example, if two arguments aren't given, however I can barely pull myself to write error messages sometimes. Eventually the codebase will be updated with a unified error message style, but for now they are kind of haphazard.
if
is a standard if statement in Crow. We will not cover this that much, however we will touch on how CRO_PrimitiveFunction
s differ from their normal counterparts.
CRO_Value CRO_if (CRO_State *s, int argc, char **argv) {
int x;
CRO_Value v;
if (argc < 2) {
printf("Error");
}
for (x = 1; x <= argc; x+=2) {
/* If x is equal to argc, we are in the else statement (which is optional) */
if (x == argc) {
v = CRO_innerEval(s, argv[x]);
return v;
}
/* Otherwise the first word is a conditional, and the second is the body */
else {
v = CRO_innerEval(s, argv[x]);
if (v.type == CRO_Bool) {
if (v.value.integer) {
v = CRO_innerEval(s, argv[x + 1]);
return v;
}
}
else {
printf("Error: not boolean\n");
}
}
}
CRO_toNone(v);
return v;
}
First of all, notice that instead of having argv
be an array of CRO_Value
, it is an array of char*
. This means what you give it in crow, for example (if varname println)
are passed directly to CRO_if
. To actually get a value from that, notice the multiple calls to CRO_innerEval
. These turn a C string into a CRO_Value
by evaluating it. The compiler does this for you in a normal function, however here we need to do it ourselves. However despite not evaluating the arguments, a CRO_PrimitiveFunction
is still expected to return a CRO_Value
, every function is.
Side note, if you look through the Crow codebase, you'll notice there are different eval functions, most notably CRO_eval
, CRO_innerEval
and CRO_evalCommand
. They are all different and should be treated as such. CRO_evalCommand
is the actual eval
function as passed to Crow, so you never need to use this one. CRO_eval
and CRO_innerEval
are both closely related, in fact CRO_eval
calls CRO_innerEval
. CRO_eval
basically adds a layer on top of CRO_innerEval
to handle multiple statements, for example the string "(println 1) (println 2)"
will work with CRO_eval
since it will stop and evaluate it after the first function call is read. However CRO_innerEval
would only execute the (println 1)
since it expects to only evaluate one statement. In a primitive function, CRO_innerEval
is perfect, since the values passed to the function are already split up by the compiler, however CRO_eval
would work just as well. If you are in doubt, just use CRO_eval
.
Okay, so now you've written your function, how do you get it into Crow. There are multiple ways to do this, and it will be split up for each.
In this case, you need to run CRO_exposeFunction
or CRO_exposePrimitiveFunction
after the creation of the CRO_State
. This will looks as follows:
CRO_exposeFunction(s, "rand", CRO_rand);
CRO_exposeFunction(s, "do-times", CRO_doTimes);
CRO_exposePrimitiveFunction(s, "if", CRO_if);
Notice, they follow the same structure. The arguments are first, the CRO_State
, then the name of the function as it will be called in Crow, and finally the C function it maps to. After this, the function will be exposed to Crow. Do note however all the Crow standard functions are exposed in the CRO_exposeStandardFunctions
function. If you are embedding Crow in another application, you can add your expose statements after a call to CRO_exposeStandardFunctions
, if you are writing for the Crow codebase, just add your functions into the function.
In this case, it is a dynamically loaded library, so it will have to first be loaded by Crow. Within a Crow file or on the REPL use the following functions to load a C function from a dynamic library.
(defvar library (load-library "theLibrary.dll")) ;; Load the library into crow and save it in the variable 'library'
(defvar our-function (get-function library "OurCFunctionName")) ;; Now get the C function.
Please notice the C function name is the EXACT name given in C. In this example, the function our-function
would call CRO_Value OurCFunctionName (CRO_State *s, int argc, CRO_Value *argv)
within the dynamic library theLibrary.dll
.
Assuming we have an array, CRO_Value arr
, we would access its contents with arr.value.array[..]
, which is an array of CRO_Value
.
CRO_State
used to be much more important to functions when we evaluated everything ourselves, however now, it is mostly there just in case. If you don't see a reason to use it, just ignore it.
You shouldn't do this. However if you must you can find the data held in the CRO_State
, check functions like defVar
or set
or current-scope
for examples of how to do this. I recommend this ONLY in a case where you ABSOLUTELY CANNOT FUNCTION without getting a variable or setting one. AGAIN LITERALLY PLEASE DON'T DO THIS!
Please feel free to contact me (Bravotic) with any issues you may be having. My contact information can be found on my website, or bravotic \[at\] aaathats3as \[dot\] com
Thank you for reading and happy hacking!