11. STL Containers

11. STL Containers #

11.1. Optionals #

The std::optional<T> will be converted into a null or the type that it can hold. This also works when you call C++ function from Wren that accepts std::optional<T>. You can either call that function with null or with the type T.

11.1.1. Limitations #

Passing std::optional via non-const reference is not allowed. Also passing as a pointer or a shared pointer does not work either. Only passing as a plain type (copy) or a const reference is allowed.

11.2. Variants #

Using std::variant is nothing special. When you pass it into Wren, what happens is that this library will check what type is being held by the variant, and then it will pass it into the Wren code. The Wren will not get the variant instace, but the value it holds! For example, passing std::variant<bool, int> will push either bool or int into Wren.

The same goes for getting values from Wren as variant. Suppose you have a C++ member function that accepts the variant of std::variant<bool, int> then when you call this member function in Wren as foo.baz(true) or foo.baz(42) it will work, but foo.baz("Hello") won’t work because the variant does not accept the string!

Consider the following example:

class Foo {
public:
    Foo() {
        ...
    }

    void baz(const std::variant<bool, std::string>& value) {
        switch (value.index()) {
            case 0: {
                // Is bool!
                const auto std::get<bool>(value);
            }
            case 1: {
                // Is string!
                const auto std::get<std::string>(value);
            }
            default: {
                // This should never happen.
            }
        }
    }
}

wren::VM vm = ...;
auto& m = vm.module(...);
auto& cls = m.klass<Foo>("Foo");
cls.func<&Foo::baz>("baz"); // Nothing special, just like any other functions

And the Wren code:

import "test" for Foo

var foo = Foo.new()
foo.baz(false) // ok
foo.baz("Hello World") // ok
foo.baz(123.456) // error

11.2.1. Limitations #

Passing std::variant via non-const reference is not allowed. The following code will not work. Also passing as a pointer or a shared pointer does not work either. Only passing as a plain type (copy) or a const reference is allowed.

class Foo {
public:
    Foo() {
        ...
    }

    void baz(std::variant<bool, std::string>& value) {
        ...
    }
}

wren::VM vm = ...;
auto& m = vm.module(...);
auto& cls = m.klass<Foo>("Foo");
cls.func<&Foo::baz>("baz"); // Will not work

11.3. Sequences #

WrenBind17 supports the following sequence containers: std::vector, std::list, std::deque, and std::set. By default all of them are converted into native Wren lists. This means that when you pass (or some C++ function returns) any of these containers, they are converted into Wren lists. Any modification to that list in Wren has no effect on the C++ container passed/returned. Wren lists are not the same object as the STL containers.

However, you can add this container to Wren VM as a foreign class. In that case the instance of the C++ container you pass into Wren will become a foreign class, therefore modifying the “list” (a class in reality) will also modify the C++ container -> they are the same object.

This only works if you pass the container (or return from a C++ function) via a non-cost reference, pointer, or a shared pointer. Passing (or returning) via copy will create a copy of that C++ container. This check (whether to convert it to a native list or as a foreign class instance) happens at the runtime.

Consider this following table.

Pass/return type Added as a foreign class Not added as a foreign class
Pass by a copy Copy of the container and pushed to Wren as a foreign class Converted to native list
Pass by a reference Pushed to Wren as a foreign class with no copy BadCast exception
Pass by a const reference Copy of the container and pushed to Wren as a foreign class Converted to native list
Pass by a (const) pointer Pushed to Wren as a foreign class with no copy BadCast exception
Pass by a shared pointer Pushed to Wren as a foreign class with no copy BadCast exception

11.3.1. Native lists #

By default all of std::vector, std::list, std::deque, and std::set are converted to Wren lists. This also works the other way around -> getting a list either by calling a C++ function that accepts a list or returning a list by calling a Wren function.

For example, you can return this native list into a std::vector of std::variant type.

class Main {
    static main() {
        return [null, 123, true, "Hello World"]
    }
}
wren::VM vm;

vm.runFromSource("main", /* code from above */);
auto func = vm.find("main", "Main").func("main()");

auto res = func();
assert(res.isList());
auto vec = res.as<std::vector<std::variant<std::nullptr_t, double, bool, std::string>>>();

assert(vec.size() == 4);
assert(std::get<std::nullptr_t>(vec[0]) == nullptr);
assert(std::get<double>(vec[1]) == 123.0);
assert(std::get<bool>(vec[2]) == true);
assert(std::get<std::string>(vec[3]) == "Hello World");

Note

In the above example we are getting std::vector<std::variant<...>>. If you add this type as a foreign class, the above example would run in the same way. This is because if you add this type as a foreign class, that only affects pushing that type to Wren. To not to use native lists, you will have to do something like this:

list = VectorOfVariant.new()
list.push(null)
list.push(123)
list.push(true)
list.push("Hello World")
return list

11.3.2. Lists as foreign classes #

If you wish to add the container of some specific type as a foreign class, you can use the following method to do so:

Wren::VM vm;
auto& m = vm.module("std");
wren::StdVectorBindings<int>::bind(m, "VectorInt");

The wren::StdVectorBindings is just a fancy wrapper that adds functions into the class. I highly recommend going through the wrenbind17/include/wrenbind17/std.hpp file to see how exactly this works.

And the usage of that in Wren:

import "std" for VectorInt

var v = VectorInt.new()
v.add(42) // Push new value
v.insert(-1, 20) // Insert at the end
v.contains(42) // returns true
v.pop() // Remove last element and returns it
v.count // Returns the length/size
v.size() // Same as the count
v.clear() // Removes everything
v.removeAt(-2) // Removes the 2nd element from back and returns it
v[0] = 43 // Set specific index (negative indexes not supported!)
System.print("Second: %(v[1])") // Get specific index (no negative indexes!)
for (item in v) { // Supports iteration
    System.print("Item: %(item)") // Prints individual elements
}

11.3.3. Custom list from scratch #

If you read through the Iterator Protocol section on official Wren documentation, then you know that you need to implement at least 2 functions:

  • iterate(_)
  • iteratorValue(_)

Let’s implement all of them for std::vector<int>! First we need the iterate() function. This function must accept already existing iterator or a null. To do this, we will use std::variant and an external function that will be bind to Wren via funcExt.

typedef typename std::vector<int>::iterator Iterator;
typedef typename std::vector<int> Vector;

static std::variant<bool, Iterator> iterate(
        Vector& self, // this
        std::variant<std::nullptr_t, Iterator> other
    ) {

    // Check if "other" variant is NOT nullptr
    if (other.index() == 1) {
        // Get the 2nd template, the iterator
        auto it = std::get<Iterator>(other);
        ++it;
        if (it != self.end()) {
            // Return the next position
            return {it};
        }

        // Once we reach the end, we must return false
        return {false};
    } else {
        // No iterator supplied, the variant is null,
        // then return the start of the vector
        return {self.begin()};
    }
}

Next, we need the iteratorValue() function. This one is very simple:

static int iteratorValue(Vector& self, std::shared_ptr<Iterator> other) {
    // You could replace the shared_ptr with a simple copy value.
    // But that depends on you.
    auto& it = *other;
    return *it;
}

And then bind it!

wren::VM vm;
auto& m = vm.module("std");
auto& cls = m.klass<Vector>("VectorInt");
cls.ctor<>();
cls.funcExt<&iterate>("iterate");
cls.funcExt<&iteratorValue>("iteratorValue");

That’s all you need to implement your custom list and use it inside of a for loop!

11.3.4. Custom lists and operator [] #

To get the operator [] working, you need two functions to set and to get the index.

typedef typename std::vector<int>::iterator Iterator;
typedef typename std::vector<int> Vector;

static void setIndex(Vector& self, size_t index, int value) {
    self[index] = value;
}

static int getIndex(Vector& self, size_t index) {
    return self[index];
}

And then bind it!

wren::VM vm;
auto& m = vm.module("std");
auto& cls = m.klass<Vector>("VectorInt");
cls.ctor<>();
cls.funcExt<&getIndex>(wren::OPERATOR_GET_INDEX);
cls.funcExt<&setIndex>(wren::OPERATOR_SET_INDEX);

The enum values you supply instead of function names will create special bindings for overloading operators. Wren will see the following:

// Autogenerated
class VectorInt {
    ...
    foreign [index]         // Get
    foreign [index]=(other) // Set
}

11.4. Maps #

WrenBind17 supports the following key-value containers: std::map and std::unordered_map. By default all of them are converted into native Wren maps. This means that when you pass any of these containers, they are converted into Wren maps. Any modification to that map in Wren has no effect on the C++ container passed. Wren maps are not the same object as the STL containers.

It is not possible to convert a native Wren map into a C++ map. This is a limitation of the Wren language. However, you can use wren::Map that will hold a reference to the Wren native map. You can use this class to retrieve values, remove keys, check if key exists, or get the size of the map.

You can add the C++ map container to Wren VM as a foreign class. In that case the instance of the C++ container you pass into Wren will become a foreign class, therefore modifying the “map” (a class in reality) will also modify the C++ container -> they are the same object.

This only works if you pass the container via a non-cost reference, pointer, or a shared pointer. Passing (or returning) via copy will create a copy of that C++ container. This check (whether to convert it to a native map or as a foreign class instance) happens at the runtime.

Consider this following table.

Pass/return type Added as a foreign class Not added as a foreign class
Pass by a copy Copy of the container and pushed to Wren as a foreign class Converted to native map
Pass by a reference Pushed to Wren as a foreign class with no copy BadCast exception
Pass by a const reference Copy of the container and pushed to Wren as a foreign class Converted to native map
Pass by a (const) pointer Pushed to Wren as a foreign class with no copy BadCast exception
Pass by a shared pointer Pushed to Wren as a foreign class with no copy BadCast exception

11.4.1. Native maps #

As mentioned above, it is not possible to get a native map from Wren and convert it into STL container. This is because the Wren low level API does not allow iterating over the map. Therefore, WrenBind17 provides wren::Map container that works on top of wren::Handle (it is a reference and affects the garbage collector).

You can use wren::Map to get values via key, remove keys, check if key exists, or get the size of the entire map. Example code below.

class Main {
    static main() {
        return {
            "first": 42,
            "second": true,
            "third": "Hello World",
            "fourth": null
        }
    }

    static other(map) {
        // Do something with the map
	}
}
wren::VM vm;

vm.runFromSource("main", code);
auto func = vm.find("main", "Main").func("main()");

auto res = func();
res.is<wren::Map>(); // Returns true
res.isMap(); // Returns true

auto map = res.as<wren::Map>();

map.count(); // Returns 4

map.contains(std::string("first")); // Returns true
map.contains(std::string("fifth")); // Returns false

map.get<int>(std::string("first")); // Returns 42
map.get<bool>(std::string("second")); // Returns true
map.get<std::string>(std::string("third")); // Returns "Hello World"
map.get<std::nullptr_t>(std::string("fourth")); // Returns nullptr
map.get<Foo>(std::string("fourth")); // Throws wren::BadCast

map.erase(std::string("first")); // Returns true
map.erase(std::string("fifth")); // Returns false
map.count(); // 3

map.get<int>(std::string("first")); // Throws wren::NotFound

auto other = vm.find("main", "Main").func("other(_)");

other(map); // Pass the map to some other function

11.4.2. Maps as foreign classes #

If you wish to add the container of some specific type as a foreign class, you can use the following method to do so:

Wren::VM vm;
auto& m = vm.module("std");
wren::StdUnorderedMapBindings<std::string, std::string>::bind(m, "MapOfStrings");

And the usage of that in Wren:

import "std" for MapOfStrings

var map = MapOfStrings.new()

// Set the value using [] operator
map["hello"] = "world" 

// Access the value
var value = map["hello"]

// Exactly same as in std::map::operator[]
// If the key does not exist, then it is created
// using the default value.
// So "value2" becomes empty string!
var value2 = map["nonexisting_key"]

// Removes value by key and returns the value removed.
// If the key does not exist, the returned value is null.
var removed = map.remove("hello")  

// Check if the key exists
if (map.containsKey("hello")) {
    System.print("Key exists")
}

// Clears the map, removing all elements.
// Same as std::map::clear()
map.clear()

// Get the number of elements in the map.
// Both the function size() and the property count do the same thing.
var total = map.size()
var total = map.count

// Check if the map is empty
if (map.empty()) {
    System.print("There is nothing in the map!")
}

// Iterate over the map.
// The map has an iterator of std::map<K, T>::iterator which
// returns std::pair<K, T> pairs.
// So to access the key you have to use the key property of the pair.
// And the same goes for the value.
// This is exactly the same behavior as iterating over the map in C++
for (pair in map) {
    System.print("Key: %(pair.key) value: %(pair.value)")

11.4.3. Maps with variant value type #

Sometimes you need more than one type inside of the map, something like a Json. To do that, you can use the std::variant as the map mapped type. An example below.

typedef std::variant<int, bool, std::string, std::nullptr_t> Multitype;

class FooClass {
public:
    ...

    void useMap(std::unordered_map<std::string, Multitype>& map) {
        // Get the Multitype value by the key
        auto& multitype = map["string"]

         // Get std::string from the variant.
         // Because std::string is the 3rd template argument
         // of the std::variant Multitype, then we 
         // need to use std::get<index> to access the type!
        auto& str = std::get<2>(multitype);
    }
};

int main() {
    Wren::VM vm;
    auto& m = vm.module("std");
    wren::StdUnorderedMapBindings<std::string, Multitype>::bind(m, "MapOfMultitypes");

    auto& cls = m.klass<FooClass>("FooClass");
    cls.func<&FooClass::useMap>("useMap");

    vm.runFromSource(...);

    return 0;
}

And the Wren code for the above map of variants:

import "std" for MapOfMultitypes, FooClass

var map = MapOfMultitypes.new()
map["string"] = "world"
map["int"] = 123456
map["null"] = null // Will become std::nullptr_t
map["boolean"] = true

for (pair in map) {
    System.print("Key: %(pair.key) value: %(pair.value)")
}

// Pass the foo to some custom class
var foo = FooClass.new()
foo.useMap(map)

11.4.4. Custom maps #

To create a custom map you will need to implement the Iterator Protocol and the [] operator.

Let’s start with the basics, create a wren VM and bind the map (with the key and value types defined!) to the Wren. You can’t create a generic map, Wren does not support that. If you want to have different map types with different key types or value types, you will need to do this multiple times. That’s what StdMapBindings<K, T> and StdUnorderedMapBindings<K, T> are designed for.

typedef std::unordered_map<std::string, int> MapOfInts;

int main() {
    wren::VM vm;
    auto& m = vm.module("mymodule");
    auto& cls = m.klass<MapOfInts>("MapOfInts");
    cls.ctor(); // Empty default constructor

    // ...

    return 0;
}

Now, the iterator and iterator value. This is based on the Wren requirements to implement a class that can be iterated. See Iterator Protocol for more information.

static std::variant<bool, MapOfInts::iterator> iterate(
        MapOfInts& self, 
        std::variant<std::nullptr_t, MapOfInts::iterator> other) {

    // If the variant holds "1" then the value being hold is a MapOfInts::iterator
    if (other.index() == 1) {
        auto it = std::get<MapOfInts::iterator>(other);
        ++it;
        if (it != self.end()) {
            return {it};
        }

        return {false};
    } 
    // Otherwise the variant holds "0" therfore a null
    else {
        return {self.begin()};
    }
}

// The "value_type" is the std::pair<K, T> of the MapOfInts
static MapOfInts::value_type iteratorValue(
        MapOfInts& self, 
        std::shared_ptr<MapOfInts::iterator> other) {

    // This simply returns the iterator value which is the std::pair
    auto& it = *other;
    return *it;
}

You will also need these two functions to access the key and the value type of the pairs during iteration.

// The "key_type" is the std::string and "mapped_type" is the int
static const MapOfInts::key_type& pairKey(MapOfInts::value_type& pair) {
    return pair.first;
}

static const MapOfInts::mapped_type& pairValue(MapOfInts::value_type& pair) {
    return pair.second;
}

Furthemore accessing or setting the values by key using the operator [] can be done as the following:

static void setIndex(MapOfInts& self, const MapOfInts::key_type& key, MapOfInts::mapped_type value) {
    self[key] = std::move(value);
}

static MapOfInts::mapped_type& getIndex(MapOfInts& self, const MapOfInts::key_type& key) {
    return self[key];
}

You will have to then bind these functions above to the MapOfInts class.

typedef std::unordered_map<std::string, int> MapOfInts;

int main() {
    wren::VM vm;
    auto& m = vm.module("mymodule");
    auto& cls = m.klass<MapOfInts>("MapOfInts");
    cls.ctor(); // Empty default constructor

    // These two functions must be named exactly like this, Wren requires these names.
    cls.funcExt<&iterate>("iterate");
    cls.funcExt<&iteratorValue>("iteratorValue");

    // These two functions add the operator [] functionality.
    cls.funcExt<&getIndex>(wren::ForeignMethodOperator::OPERATOR_GET_INDEX);
    cls.funcExt<&setIndex>(wren::ForeignMethodOperator::OPERATOR_SET_INDEX);

    // Bind the iterator of this map, without this
    // you cannot pass the iterator between Wren and C++
    auto& iter = m.klass<MapOfInts::iterator>("MapOfIntsIter");
    iter.ctor();

    // Bind the pair of this map too. You need this
    // so you can access the keys and values during iteration.
    auto& pair = m.klass<MapOfInts::value_type>("MapOfIntsPair");
    pair.propReadonlyExt<&pairKey>("key");
    pair.propReadonlyExt<&pairValue>("value");

    return 0;
}

Sample Wren usage:

import "mymodule" for MapOfInts

var map = MapOfInts.new()

map["first"] = 123
map["second"] = 456
var second = map["second"]


for (pair in map) {
    System.print("Key: %(pair.key) value: %(pair.value)")
}