-
Notifications
You must be signed in to change notification settings - Fork 5
Type Specification
wll-interface supports three categories of types: scalar types, tensor types, and sparse array types. The objects of these types are passed through LibraryLink from and to Wolfram Language. (Refer to object initialization for how to initialize tensors and sparse arrays on C++ side.)
Schematically, a LibraryLink function takes a fixed number of arguments (zero or more), and return one result. The types of the arguments and the result have to be specified both in the C++ source code and in Wolfram Language where the function is loaded. In C++ source code, the function should be declared as
ReturnType f(ArgumentTypes...);
and in Wolfram Language, the types are indicated as the third and fourth argument to LibraryFunctionLoad
:
LibraryFunctionLoad[(*libpath*), (*name*), {ArgumentTypes...}, ReturnType]
Note that the types defined in C++ and Wolfram Language do not have a one-to-one match. For instance, real number in Wolfram Language is of Real
type, typically double precision floating-point type, which is equivalent to double
in C++. However, float
, single precision floating-point type, does not have a Wolfram Language counterpart. wll-interface solves this inconsistency by doing automatic type conversions during LibraryLink communications. For example, whenever a function is declared to return a float
, wll-interface will first convert it to a double
, then pass the number through LibraryLink. This feature frees the users from worrying about type differences and eliminates potential errors.
Wolfram Language | LibraryLink | C++ no conversion | C++ with conversion |
---|---|---|---|
"Boolean" |
mbool |
bool |
|
Integer |
mint |
int32_t /int64_t * |
Other integral types ** |
Real |
mreal |
double |
float , long double
|
Complex |
mcomplex |
std::complex<double> |
std::complex<T> |
"UTF8String" |
char* |
std::string |
const char* |
"Void" |
void |
void |
* Depends on the target platform.
** Types that satisfies std::is_integral_v<T> == true
.
For example, a LibraryLink function takes a real number and an integer, multiplies them together, and return the result as a real number. In C++ source code, this function can be declared as
double multiply(double, int);
and in Wolfram Language, this function is loaded as
multiply = LibraryFunctionLoad[(*libpath*), "wll_multiply", {Real, Integer}, Real];
wll-interface provides a template class tensor
to handle all LibraryLink tensor objects and it is easy to use. To avoid potential name collisions, tensor
class is placed under wll
namespace (so does other wll-interface classes and functions).
A tensor has two fixed properties: the type of its elements, and the rank. They are considered fixed because they are determined at compile time, i.e. we have to decide the element type and the rank when we are writing C++ code.
A tensor with element type T
and rank R
has type wll::tensor<T, R>
. For example, a matrix of integers has type wll::tensor<int, 2>
. Since low-rank tensors are being used most frequently, wll-interface defines wll::list<T>
to be wll::tensor<T, 1>
, and defines wll::matrix<T>
to be wll::tensor<T, 2>
.
The following C++ code declares a function sum that takes a list of real numbers, and return a single real number:
double sum(wll::list<double> input);
or equivalently:
double sum(wll::tensor<double, 1> input);
In Wolfram Language, a tensor type is represented by {type, rank}
, therefore, the sum
function is loaded as:
LibraryFunctionLoad[(*libpath*), "wll_sum", { {Real, 1} }, Real]
A tensor also has the dimensions as its property. We can construct the tensor by explicitly giving the dimenions, e.g.
wll::tensor<int, 3> mytensor({4, 6, 2});
declares a 3-dimensional tensor of integers with dimensions 4×6×2 and initializes it with zeros. To get the dimensions information of a tensor, use .dimension(level)
method. Here mytensor.dimension(0)
gives 4, while mytensor.dimension(2)
gives 2.
In addition, when a tensor is passed from Wolfram Language, wll-interface will fetch its dimensions using LibraryLink APIs, and construct the tensor. For instance, if a tensor of 10 real numbers is passed from Wolfram Language to our sum
function in the previous section, input
will be automatically initialized to store those 10 real numbers.
Once we have a tensor, we need to get access to its elements by indices. For wll::tensor
objects, accessing is done by parenthesis, and zero-based numbering (C style) is used. Note that Wolfram Language uses one-based numbering.
For example, if we have a wll::list
named mylist
, the 5th element in this list is represented by mylist(4)
. If we have a wll::matrix
named mymatrix
, the i-th row j-th column element in this matrix is represented by mymatrix(i-1,j-1)
.
Similar to C++ standard library containers, wll-interface offers .at
method for the tensors, i.e. t.at(i,j)
has the same meaning as t(i,j)
. Further more, negative indices are valid in wll-interface, where index -i
means the i-th element counting from the back.
Now we are ready to complete our sum
function. The C++ source code is as follows:
#include "wll_interface.h" // include wll-interface
double sum(wll::list<double> input) // "input" is a list of real numbers
{
double result = 0.0; // initialize result to zero
for (int i = 0; i < input.dimension(0); ++i)
result += input(i);
return result;
}
DEFINE_WLL_FUNCTION(sum) // defines LibraryLink function "wll_sum"
wll-interface provides a template class wll::sparse_array
to handle all LibraryLink sparse array objects, which have two fixed properties, type and rank, just like wll::tensor
.
In Wolfram Language, a sparse array is represented as LibraryDataType[SparseArray, type, rank]
, or as simple as LibraryDataType[SparseArray]
when we do not care about its value type and rank, e.g. as the return type.
wll::sparse_array
uses the same storage format, Compressed Sparse Row (CSR), as is implemented by Wolfram Language.
Four methods of wll::sparse_array
can be called to retrieve the internal data:
Method | Return type | Meaning |
---|---|---|
.implicit_value() |
value_type |
the implicit value |
.row_indices_pointer() |
const size_t* |
pointer to the row indices array |
.columns_pointer() |
const _column_t* |
pointer to the columns array |
.values_pointer() |
const value_type* |
pointer to the explicit values array |
The i-th element (starting from 0) of row indices array stores the number of explicit elements before the end of the i-th row. Columns array stores the column parts (all but the first index) of the explicit elements in order. Explicit values array stores the values of explicit elements in order. Note that 1-dimensional arrays are stored by CSR as if they are 1×N matrices.
wll::sparse_array
have the same methods as wll::tensor
to accessing the elements. But their return types are not the same, and they have slightly different usages on non-constant sparse arrays. In short, when define an identifier to be the reference to an element in sparse arrays, auto
should be used (instead of auto&
). For example,
wll::sparse_array<int, 2> s({10, 10}); // a 10*10 sparse array
auto e = s(2, 3); // e is a reference to s(2, 3), then
e = 5; // change the value of s(2, 3) to 5;
// or simply,
s(2, 3) = 12; // change it to 12 instead.
We can construct a sparse array from a tensor of the same value type and rank. For example,
wll::tensor<int, 3> t0({4, 5, 6});
wll::sparse_array<int, 3> s0(t0);
And convert a sparse array back to a tensor. For example,
wll::sparse_array<double, 2> s1({8, 8});
wll::tensor<double, 2> t1(s1);
We have talked about how to construct a tensor by giving dimensions. But we sometimes want to initialize it with values other than zero. To do this, we can write the initial values as the second argument, e.g.
wll::list<int> list8({8}, {3,1,4,1,5,9,2,6} ); // a list of int's
As long as the array of initial values is rectangular, we can leave the list of dimensions blank, letting it be automatically deduced, e.g.
wll::matrix<int> mat3({ }, {{1,0,0}, {0,1,0}, {0,0,1}} ); // a 3×3 matrix
The types of the initial values should always be the same, but this type can be different from the declared value type of the tensor. We can declare the above matrix with double
instead,
wll::matrix<double> mat3({ }, {{1,0,0}, {0,1,0}, {0,0,1}} ); // convert to double's
Sparse arrays have a different syntax to do initialization. Similar to Wolfram Language, we give a list of position to value rules as the second argument, where a rule is written as
wll::pos(i1, i2, ...) = val
meaning that the value should be val
on the {i0, i1, ...}
position of the sparse array.
Here, as an example, we construct a 3×3 identity matrix as a sparse array (feel free to using namespace wll;
to avoid repeating wll::
),
wll::sparse_array<int, 2> sparse3({ }, {
wll::pos(0,0) = 1,
wll::pos(1,1) = 1,
wll::pos(2,2) = 1 });
If we do not give the dimensions of the sparse array (leave it blank), it will take the smallest size that can hold the values in the list of rules (3×3 as in the example).
Note that the rules are applied to the sparse array in order. Therefore, if the list has duplicate rules in terms of positions, only the last one is effective:
wll::sparse_array<int, 2> s({10, 10}, {
wll::pos(2, 3) = 5,
wll::pos(2, 3) = 1,
wll::pos(2, 3) = 6 }); // s(2, 3) == 6