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.
.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.
MODULE module-nameFUNCTION name min_pos max_pos key1 key2 key3 ...PROCEDURE name min_pos max_pos key1 key2 key3 ...where
module-name is a unique module identifiername is a (case-sensitive) routine name, used both for FL and
C++. Object methods contain ::, which are converted to __ for the
C++ name, eg. the FL object method myclass::open should be defined
in the C++ source as myclass__open.min_pos is the minimum number of positional arguments requiredmax_pos is the maximum number of positional arguments allowed (use
-1 for no limit)key1 key2 key3 ... is the list of keywords acceptedDLM 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 <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.
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.)
FL variables are represented by the c_data class. The constructor of
the class creates an undefined variable.
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()
set_xx methods:| 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 |
create_struct(const std::string& name)
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.
create_struct(const c_struct_def& sd)
Here sd is a structure definition object. The structure fields are
copied from the structure definition fields.
create_pointer(int alloc)
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);
uvarivar containing (long) 123svar containing (string) "123"pvar containing a pointer referencensvar containing a (zeroed) my_data structureasvar containing an anonymous structure,
with field values 123, "123" and a pointer reference.create_array(e_type type, int ndim, const t_im* dims)
create_struct_array(const std::string& name, int ndim, const t_im* dims)
create_struct_array(const c_struct_def& sd, int ndim, const t_im* dims)
Here
type is the symbolic name of the typendim is the number of dimensionsdims is an array containing the actual dimensionsThe 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);
ivar of size 4x5, containing (long) zerosnsvar of size 10, containing (zeroed) my_data structuresThe 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();
type is c_data::T_I4size is 1i4p points to the value of ivar, *i4p is (long) 123Using 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
T is the desired C++ typeN is the group size, which denotes how many elements will be
accessed in a single call (eg. if you have x,y,z coordinates in a
coord(3, 100) array, you can get the three x,y,z values at the same
time). If omitted, the default value is 1.data is the input dataGetting 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
...
}
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()
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()
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()
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()
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()
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
}
}
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
name is the name of the structure, should be empty for anonymous
structurestag is the field nameval is the field valuebase is the name of the base structureExample:
// 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
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);
}
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 |
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
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.
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