Skip to content

Type Specification

njpipeorgan edited this page Aug 9, 2018 · 22 revisions

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.

Scalar types

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];

Tensor types

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).

Type and rank

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]

Dimensions

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.

Accessing

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"

Sparse array types

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.

Storage format

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.

Accessing

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.

Convert to and from tensors

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);

Object Initialization

Tensors

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

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