fe/doc/capi.md

3.8 KiB

C API

To use fe in a project a fe_Context must first be initialized; this is done by using the fe_open() function. The function expects a block of memory (typically greater than 16kb), the block is used by the context to store objects and context state and should remain valid for the lifetime of the context. fe_close() should be called when you are finished with a context, this will assure any ptr objects are properly garbage collected.

int   size = 1024 * 1024;
void *data = malloc(size);

fe_Context *ctx = fe_open(data, size);

/* ... */

fe_close(ctx);
free(data);

Running a script

To run a script it should first be read then evaluated; this should be done in a loop if there are several root-level expressions contained in the script. fe_readfp() is provided as a convenience to read from a file pointer; fe_read() can be used with a custom fe_ReadFn callback function to read from other sources.

FILE *fp = fopen("test.fe", "rb");
int gc = fe_savegc(ctx);

for (;;) {
  fe_Object *obj = fe_readfp(ctx, fp);

  /* break if there's nothing left to read */
  if (!obj) { break; }

  /* evaluate read object */
  fe_eval(ctx, obj);

  /* restore GC stack which would now contain both the read object and
  ** result from evaluation */
  fe_restoregc(ctx, gc);
}

fclose(fp);

Calling a function

A function can be called by creating a list and evaulating it; for example, we could add two numbers using the + function:

int gc = fe_savegc(ctx);

fe_Object *objs[3];
objs[0] = fe_symbol(ctx, "+");
objs[1] = fe_number(ctx, 10);
objs[2] = fe_number(ctx, 20);

fe_Object *res = fe_eval(ctx, fe_list(ctx, objs, 3));
printf("result: %g\n", fe_tonumber(ctx, res));

/* discard all temporary objects pushed to the GC stack */
fe_restoregc(ctx, gc);

Creating a cfunc

A cfunc can be created by using the fe_cfunc() function with a fe_CFunc function argument. The cfunc can be bound to a global variable by using the fe_set() function. cfuncs take a context and argument list as its arguments and returns a result object. The result should never be NULL; in the case of wanting to return nil the value returned by fe_bool(ctx, 0) should be used.

The pow function from math.h could be wrapped as such:

static fe_Object* f_pow(fe_Context *ctx, fe_Object *arg) {
  float x = fe_tonumber(ctx, fe_nextarg(ctx, &arg));
  float y = fe_tonumber(ctx, fe_nextarg(ctx, &arg));
  return fe_number(ctx, pow(x, y));
}

fe_set(ctx, fe_symbol(ctx, "pow"), fe_cfunc(ctx, f_pow));

The cfunc could then be called like any other function:

(print (pow 2 10))

Creating a ptr

The ptr object type is provided to allow for custom objects. By default no type checking is performed and thus pointers must be wrapped by the user and tagged to assure type safety if more than one type of pointer is used.

A ptr object can be created by using the fe_ptr() function.

The gc and mark handlers are provided for dealing with ptrs regarding garbage collection. Whenever a ptr is marked by the GC the mark handler is called on it — this is useful if the ptr stores additional objects which also need to be marked via fe_mark(). The gc handler is called on the ptr when it becomes unreachable and is garbage collected, such that the resources used by the ptr can be freed. The handlers can be set by setting the relevant fields in the struct returned by fe_handlers().

Error handling

When an error occurs the fe_error() is called; by default, the error and stack traceback is printed and the program exited. If you want to recover from an error the error handler field in the struct returned by fe_handlers() can be set and longjmp() can be used to exit the handler; the context is left in a safe state and can continue to be used. New fe_Objects should not be created inside the error handler.