FLX: FL eXternal development interface

Dynamically loadable modules

FL supports the DLM interface to external C++ functions. The routines added this way are equivalent to system routines.

The DLM interface consists of two files: a .flm module description file and a .so shared object, which will be automatically loaded by FL if needed.

Module description files (.flm)

Module description files are text files describing the DLM routines. The routines are defined at startup, based on the content of the .flm files found in the current directory and in the !DLM_PATH search path.

An .flm file must contain one MODULE line and at least one FUNCTION or PROCEDURE line. Other lines are ignored.

where

Shared libraries

DLM shared libraries must be in the same directory as the .flm files, and have the same basename with an .so extension.

The libraries should be created by a GCC 4.3.x compatible C++ compiler. The easiest way is using the MAKE_DLL command.

The C++ interface

The <flx.h> header, available in the $FL_DIR/include directory, should be included in the DLM source file.

C++ mangles function names. To avoid this "uglification", the interface routines should be declared extern C. The FLX_ROUTINE convenience macro creates both this declaration and the function definition, eg:

FLX_ROUTINE(fname)
{
    // function body
}

defines function fname, which can be called from FL simply as fname.

The FLX interface is in the C++ namespace flx. Everything from the interface should be referred by the flx:: prefix, or alternatively, the flx namespace should be imported by a "using namespace flx;" line. In this document, the latter is supposed.

Data types

The following table summarizes all available data types:

symbolic name type C++ type
c_data::T_I1 8 bit signed integer t_i1
c_data::T_U1 8 bit unsigned integer t_u1
c_data::T_I2 16 bit signed integer t_i2
c_data::T_U2 16 bit unsigned integer t_u2
c_data::T_I4 32 bit signed integer t_i4
c_data::T_U4 32 bit unsigned integer t_u4
c_data::T_I8 64 bit signed integer t_i8
c_data::T_U8 64 bit unsigned integer t_u8
c_data::T_F4 32 bit float t_f4
c_data::T_F8 64 bit float t_f8
c_data::T_C4 32 bit float complex t_c4
c_data::T_C8 64 bit float complex t_c8
c_data::T_STRING string t_string
c_data::T_STRUCT structure -
c_data::T_OBJECT object reference -
c_data::T_POINTER pointer reference -
c_data::T_UNDEF undefined -
c_data::T_IM 32 or 64 bit signed integer t_im
c_data::T_IF 32 or 64 bit signed integer t_if

(The latter two can be used for memory and file size and offset representation.)

Variables

FL variables are represented by the c_data class. The constructor of the class creates an undefined variable.

Creating variables

Copying an other variable:

assign(const c_data* src)

Moving the content from an other variable, which will be undefined afterwards:

move(c_data* src)

Converting to given type, preserving the structure of the source:

void convert(c_data* src, e_type type)

Converting to t_im (for scalars only):

t_im convert_to_im()

Freeing:

undef()

Creating scalar variables

symbolic name method
c_data::T_I1 set_i1
c_data::T_U1 set_u1
c_data::T_I2 set_i2
c_data::T_U2 set_u2
c_data::T_I4 set_i4
c_data::T_U4 set_u4
c_data::T_I8 set_i8
c_data::T_U8 set_u8
c_data::T_F4 set_f4
c_data::T_F8 set_f8
c_data::T_C4 set_c4
c_data::T_C8 set_c8

The usual automatic structure definition rules apply here: if structure name is not defined, FL tries to find and run the name__define routine. The structure fields are zeroed.

Here sd is a structure definition object. The structure fields are copied from the structure definition fields.

If alloc is zero, a NULL pointer is created. The referenced heap variable will be undefined.

Objects can be created only by the c_routine_call class (with OBJ_NEW) as parameters must be passed to the object INIT method.

The previous content of the variable is always destroyed.

Example:

c_data uvar;

c_data ivar;
ivar.set_i4(123);

c_data svar;
svar.set_string("123");

c_data pvar;
pvar.create_pointer(1);

c_data nsvar;
nsvar.create_struct("my_data");

c_struct_def sd("");
sd.add_field("long", &ivar);
sd.add_field("string", &svar);
sd.add_field("pointer", &pvar);

c_data asvar;
asvar.create_struct(sd);

Creating array variables

Here

The previous content of the variable is always destroyed and the new array is zeroed.

Example:

t_im dims[2];
dims[0]=4;
dims[1]=5;

c_data ivar;
ivar.create_array(c_data::T_I4, 2, dims);

dims[0]=10;
c_data nsvar;
nsvar.create_struct_array("my_data", 1, dims);

Accessing variable data: generic

The following general methods work for any type of data:

c_data::e_type get_type()

returns the symbolic name of the data type

t_im get_size()

returns the number of elements of the data (0 for undefined, 1 for scalars and >=1 for arrays)

void* get_address()

returns the pointer to the actual data (NULL for undefined)

Example:

c_data ivar;
ivar.set_i4(123);

c_data::e_type type=ivar.get_type();
t_im size=ivar.get_size();
t_i4* i4p=(t_i4*)ivar.get_address();

Using the previous general access method, a type name and a void* pointer is returned and the pointer must be cast to the proper type based on the type value. There is a c_elem_access class which can return properly typed pointers, and (optionally) can convert the original data to a different type on the fly (in this case the pointer refers to temporary memory, not to the original data).

Constructor:

c_elem_access<T, N> elem(c_data* data)

where

Getting a single value is done by the [i] operator, which returns a T& reference to the ith value. Getting multiple values is done by the (i) operator, which returns a T* pointer to the ith group.

Example:

// coord is a 3x100 numeric array

c_elem_access<c_data::t_f8> elem1(&coord);

for (int j=0; j<300; j++)
    { t_f8 num=elem1[j];   // j-th element, converted to t_f8 if needed
      ...
    }

c_elem_access<c_data::t_f8, 3> elem3(&coord);

for (int j=0; j<100; j++)
    { t_f8* ptr=elem3(j);   // j-th triplet, converted to t_f8 if needed
      ...
    }

Accessing variable data: scalars

The value of numeric or string scalars can be read by the get_xx methods. An error is thrown if the variable is not scalar or its type does not correspond to the type in the method name:

symbolic name method
c_data::T_I1 get_i1
c_data::T_U1 get_u1
c_data::T_I2 get_i2
c_data::T_U2 get_u2
c_data::T_I4 get_i4
c_data::T_U4 get_u4
c_data::T_I8 get_i8
c_data::T_U8 get_u8
c_data::T_F4 get_f4
c_data::T_F8 get_f8
c_data::T_C4 get_c4
c_data::T_C8 get_c8
c_data::T_STRING get_string

For strings, there is an other method for getting read-only reference (this method avoids string copying):

const c_data::t_string& get_string_ref()

Acessing variable data: arrays

The following methods work only for arrays, otherwise throw an error.

Number of array dimensions:

int get_array_ndim()

Dimensions:

const t_im* get_array_dims()

ith dimension (starting from 0):

t_im get_array_dim(int i)

Number of elements:

t_im get_array_size()

Data address:

void* get_array_address()

Checking variable properties

The following methods check data properties. If the check fails, an error is thrown.

Variable data type:

void check_type(e_type type)

Variable is defined:

void check_defined()

Variable is scalar:

void check_scalar()

Variable is array:

void check_array()

Variable is n dimensional array:

void check_array_ndim(int n)

Variable is array and the ith dimension is n:

void check_array_dim(int i, t_im n)

Variable is numeric:

void check_num()

Variable is normal (not temporary) variable:

void check_var()

Getting variable properties

The following methods return a zero/nonzero value depending on the given property:

Variable is scalar:

int is_scalar()

Variable is array:

int is_array()

Variable is set (for keywords):

int is_key_set()

Variable is true:

int is_true()

Variable is zero:

int is_zero()

Variable is temporary:

int is_tmp()

Variable is normal variable:

int is_var()

Getting structure information

The following methods return structure specific information (throw an error for non-structures).

Structure name (empty for anonymous):

const std::string& get_struct_name()

Number of structure fields:

int get_struct_ntag()

Structure field names:

const std::vector<std::string>& get_struct_tags()

Getting heap data

For scalar POINTER or OBJECT variables, returns the address of the referenced heap variable (throws an error for other types or arrays):

c_data* get_heap_var()

Example:

// var is a variable

if ( var.is_scalar() && (var.get_type()==c_data::T_POINTER) )
   { c_data* hvar=var.get_heap_var();

     if ( hvar->is_scalar() )
        { // scalar, do something
        }
     else
     if ( hvar->is_array() )
        { // array, do something
        }
     else
        { // undefined, do something
        }
   }

Structures definition

Structure definitions are created by the c_struct_def class and are used by c_data for creating structures.

Constructor:

c_struct_def(const std::string& name)

Adding fields:

void add_field(const std::string& tag, c_data* val)

Inheriting from other structures:

void inherits(const std::string& base)

here

Example:

// this is equivalent to {mystr, long:0l, inherits base, string:''}

c_struct_def sd("mystr");
sd.add_field("long", &i4);   // i4 contains a scalar long value
sd.inherits("base");
sd.add_field("string", &s);  // s contains a scalar string value

Routine arguments

The C++ prototype of a DLM routine is

void routine_name(const c_arg& args)

Arguments can be accessed through c_arg methods:

Get the number of positional arguments:

int get_npos()

Get the number of keyword arguments:

int get_nkey()

Get the ith positional argument (starting from 0 for the first argument). Returns NULL if the ith positional argument was not supplied, and throws an error if i is negative or greater than or equal to the maximum number for positional arguments. The arguments are shifted for object methods: get_pos(0) returns self, get_pos(1) returns the first argument, etc.:

c_data* get_pos(int i)

Get the keyword argument named key. Returns NULL if the keyword was not supplied.

c_data* get_key(const std::string& key)

Get the return variable (returns NULL for procedures):

c_data* get_return()

Example:

FLX_ROUTINE(my_fun)
{
    int npos=args.get_npos();
    for (int j=0; j<npos; j++)
         args.get_pos(j)->check_defined();

    c_data* key=args.get_key("KEYWORD");
    if ( key && key->is_key_set() )
       ; // keyword is present and set

    c_data* res=args.get_return();
    res->set_i4(0);
}

Arithmetic and logical operators

FL's built-in operators are accessible through the op_xxx functions.

Unary operators: void op_xxx(c_data* in, c_data* out): operator(in) -> out:

negation op_neg
bitwise not op_bit_not
logical not op_log_not

Binary operators: void op_xxx(c_data* in1, c_data* in2, c_data* out): in1 operator in2 -> out:

addition op_add
subtraction op_sub
multiplication op_mul
division op_div
modulo op_mod
exponentiation op_pow
minimum op_min
maximum op_max
equal to op_eq
not equal to op_ne
greater than or equal to op_ge
greater than op_gt
less than or equal to op_le
less than op_lt
bitwise and op_bit_and
bitwise or op_bit_or
bitwise xor op_bit_xor
logical and op_log_and
logical or op_log_or

Calling system or user routines

System or user routines can be called through the c_routine_call class.

The routine name and type (function or procedure) must be given in the constructor:

c_routine_call(const std::string& name, c_routine_call::e_type type)

Adding positional arguments:

void add_pos(c_data* val)

Adding keyword arguments:

void add_key(const std::string& key, c_data* val)

Adding return variable (functions only):

void set_return(c_data* val)

Doing the actual call:

void call()

Example:

// this is equivalent to "PLOT, DIST(10)+2, /YLOG"

c_routine_call fun_call("DIST", c_routine_call::T_FUN);
c_data var;
var.set_im(10);
fun_call.add_pos(&var);
c_data res;
fun_call.set_return(&res);
fun_call.call();

c_data two;
two.set_i4(2);
op_add(&res, &two, &res);

c_routine_call pro_call("PLOT", c_routine_call::T_PRO);
pro_call.add_pos(&res);
c_data ylog;
ylog.set_i4(1);
pro_call.add_key("YLOG", &ylog);
pro_call.call();

WARNING! DLM routines get data pointers as input. Some of these pointers refer to data on the stack or heap. If the stack or heap is reallocated during system or user routine call, these pointers become invalid, and using them may corrupt or crash the session. To avoid this problem, FL throws an error when this reallocation occurs during a DLM routine call.

Reallocation can be avoided by reserving enough space on the stack or heap before the DLM routine call. This can be easily achieved by calling the following procedures:

pro reserve_stack_space, n   ; reserves space for n stack variables
a1=(a2=(a3=(a4=(a5=(a6=(a7=(a8=0)))))))
if n gt 0 then reserve_stack_space, n-10
end

pro reserve_heap_space, n   ; reserves space for n heap variables
ptr_free, ptrarr(n, /allocate)
end

Error handling

FLX uses C++ exceptions for error handling. Errors can be thrown by the

throw_error(const std::string& msg)

function, where msg is an informative message, which will be displayed on the console.

Utility functions

Print to the console:

print(const std::string& msg)

Print to the console with newline appended:

print_nl(const std::string& msg)

Convert:

convert(t_im n, c_data::e_type src_type, void* src, c_data::e_type dst_type, void* dst)

convert n values of type src_type at address src to n values of type dst_type at address dst