6. Custom types

6. Custom types #

Wren supports adding custom types. See official documentation here. This is done via foregin classes that can have member of static functions and fields. All of foreign classes added via WrenBind17 are wrapped in a custom wrapper (wren::detail::ForeignObject<T>) that takes care of handling of instance of your custom C++ type.

In order to use your custom types, you will have to register them as a foreign classes with foreign functions into the Wren VM. This is not done globally, therefore you will have to do this for each of your Wren VM instances. Wren VMs do not share their data between instances, nor the data about foreign classes.

6.1. Type safety and shared_ptr #

All custom types are handled as a std::shared_ptr<T>. Even if you pass your custom type into Wren as a copy, it will be moved into a shared pointer. Passing shared pointers into Wren has no extra mechanism and simply the shared pointer is kept inside of the wrapper. This also ensures strong type safety. It is not possible to pass a C++ class instance of one type and get that variable back from Wren as a C++ class instance of an another type. Doing so will throw the wren::BadCast exception.

6.2 Passing values into Wren #

Passing values into Wren can be done in multiple ways, but they always end up as a shared pointer. Passing values happens when you call a Wren function from C++ and you pass some arguments, or when Wren calls a C++ foreign function that returns a type.

Returning a value from a C++ function or passing into a Wren function is done by the same mechanism, therefore there are no differences.

  • Pass/return as a copy -> Moved into std::shared_ptr<T>
  • Pass/return as a pointer -> Pointer moved into std::shared_ptr<T> with no deleter (won’t free).
  • Pass/return as a reference -> Handled as a pointer and pointer moved into std::shared_ptr<T> with no deleter (won’t free).
  • Pass/return as a const pointer -> Pointer moved into std::shared_ptr<T> with no deleter (won’t free).
  • Pass/return as a const reference -> Handled as copy and the instance is copied (duplicated) via it’s copy constructor.

TL;DR: Everything is a shared pointer.

6.3 Adding custom modules #

To bind a C++ class, you will first need to create a new module. Creating a new module is done per VM instance. If you have multiple VMs in your application, they won’t share the same modules. You would have to create the module for each of your VM instances. Copying modules between instances is not possible.

namespace wren = wrenbind17; // Alias

wren::VM vm;

// Create module called "mymodule"
wren::ForeignModule& m = vm.module("mymodule");

You can create as many modules as you want. Additionally, calling the method module(...) multiple times with the same name won’t create duplicates. For example:

wren::VM vm;

wren::ForeignModule& m0 = vm.module("mymodule");
wren::ForeignModule& m1 = vm.module("mymodule");

// m0 and m1 now point to the exact same module

Note

Modules must be used via a reference wren::ForeignModule& m = .... Copying modules is not allowed and causes compilation error.

6.3 Adding custom classes #

Once you have a custom module, you can add classes to it, any classes. Your class does not have to have any specific functions, there are no requirements. Your class does not have to have a default constructor too, you can add anything.

Let’s assume this is your C++ class:

class Foo {
public:
    Foo(const std::string& msg) {
        ...
    }

    void bar() {

    }

    int baz() const {

    }
};

You can add it in the following way:

wren::VM vm;
auto& m = vm.module("mymodule");

// Add class "Foo"
auto& cls = m.klass<Foo>("Foo");

// Define constructor (you can only specify one constructor)
cls.ctor<const std::string&>();

// Add some methods
cls.func<&Foo::bar>("bar");
cls.func<&Foo::baz>("baz");

The class functions (methods) are added as a template argument, not as the function argument. This is due to the how Wren is built. Because of this implementation, you will also get extra performance -> the pointers to the class functions are optimized at compile time -> there are no lookup maps.

And this is how you can use it:

import "mymodule" for Foo

var foo = Foo.new("Message")
foo.bar();
var i = foo.baz();

Please note that you don’t have to manually create file “mymodule.wren” and add all of your C++ foreign classes into it manually. Everything is automatically generated by the wren::VM. You can get the “raw” contents of the module that will be put into Wren by simply calling .str() on the module (e.g. vm.module("mymodule").str();). This will print the contents of that module as a Wren code with foreign classes.

6.3.1 Handling function pointer ambiguity #

In case you have multiple functions with the same name, you will have to use static_cast to explicitly tell the compiler which function you want. For example:

class Foo {
    const std::string& getMsg() const;
    std::string& getMsg();
};

wren::VM vm;
auto& m = vm.module("mymodule");
auto& cls = vm.klass<Foo>("Foo");
cls.func<static_cast<const std::string& (*)(void) const>(&Foo::getMsg)>("getMsg");

6.3.2 Static functions #

To add a static function, simply call the funcStatic instead of func as shown below:

class Log {
    static void debug(const std::string& text);
    static void error(const std::string& text);
    static void info(const std::string& text);
};

wren::VM vm;
auto& m = vm.module("mymodule");
auto& cls = m.klass<Log>("Log");
cls.funcStatic<&Log::debug>("debug");
cls.funcStatic<&Log::error>("error");
cls.funcStatic<&Log::info>("info");

6.3.3 Function from base classes #

To add a function that belongs to the base class, simply use a pointer to the base class, example as shown below:

class Asset {
public:
    std::string getName();    
};

class AssetModel: public Asset {
public:
    std::string getModelPath();    
};

wren::VM vm;
auto& m = vm.module("mymodule");
auto& cls = m.klass<AssetModel>("AssetModel");
cls.func<&Asset::getName>("getName");
cls.func<&AssetModel::getModelPath>("getModelPath");

This works as long as Asset is a base class of AssetModel. This does not work with cls.funcExt<>(), in that case your external function first argument must be the derived class.

6.3.4 Functions via external functions #

Suppose you have some C++ class that you want to add to Wren, but you can’t modify this class because it is from some other library. For example this C++ class can be from STL, or you want to add some custom behavior that does not exist in the class. In this case you can create a new function that accepts the class instance as the first parameter.

// Custom member function with "self" as this pointer
template<typename T>
bool vectorContains(std::vector<T>& self, const T& value) {
    return std::find(self.begin(), self.end(), value) != self.end();
}

// Custom static function without any "self"
template<typename T>
bool vectorFactory(const T& value) {
    std::vector<T> vec;
    vec.push_back(value);
    return vec;
}

auto& cls = m.klass<std::vector<int>>("VectorInt");
cls.ctor<>();
cls.funcExt<&vectorContains<int>>("contains");
cls.funcStaticExt<&vectorFactory<int>>("factory");
import "mymodule" for VectorInt

var a = VectorInt.new()
var b = VectorInt.factory(456) // Calls vectorFactory<T>
a.contains(123) // returns bool

“Ext” simply means that this is an external function and the first parameter must accept a reference to the class you are adding (unless it is a static function). Do not mistake this with “extern” C++ keyword. It has nothing to do with that. (Maybe there is a better word for it?)

Additionally, if you look at the vectorContains function from above, there is no “this” pointer because this is not a member function. Instead, the “this” is provided as a custom parameter in the first position. This also works with propExt and propReadonlyExt.

External functions added as funcStaticExt will be treated as static functions and do not accept the first parameter as “this”.

6.4 Abstract classes #

What if you want to pass an abstract class to Wren? You can’t allocate it, but you can only pass it around as a reference or a pointer? Imagine a specific derived “Entity” class that has a common abstract/interface class?

The only thing you have to do is to NOT to add constructor by not calling the ctor function.

wren::VM vm;
auto& m = vm.module("mymodule");

// Add class "Vec3"
auto& cls = m.klass<Entity>("Entity");
// cls.ctor<>(); Do not add constructor!
cls.func<&Entity::foo>("foo");

6.5 Adding class varialbles #

There are two ways how to add C++ class variables to Wren. One is as a simple field and the other way is as a property. Adding as a field is done by accessing the pointer to that field (which is just a relative offset to “this”). Adding as a property is done via getters and setters (the setters are optional). To Wren, there is no difference between those two and are treated equally.

6.5.1 Class variables as fields #

struct Vec3 {
    float x = 0;
    float y = 0;
    float z = 0;
};

wren::VM vm;
auto& m = vm.module("mymodule");

// Add class "Vec3"
auto& cls = m.klass<Vec3>("Vec3");
cls.ctor<>();
cls.var<&Vec3::x>("x");
cls.var<&Vec3::y>("y");
cls.var<&Vec3::z>("z");

6.5.2 Class variables as properties #

class Vec3 {
public:
    float getX() const     { return x; }
    void setX(float value) { x = value; }
    float getY() const     { return y; }
    void setY(float value) { y = value; }
    float getZ() const     { return z; }
    void setZ(float value) { z = value; }
private:
    float x = 0;
    float y = 0;
    float z = 0;
};

wren::VM vm;
auto& m = vm.module("mymodule");

// Add class "Vec3"
auto& cls = m.klass<Vec3>("Vec3");
cls.ctor<>();
cls.prop<&Vec3::getX, &Vec3::setX>("x");
cls.prop<&Vec3::getY, &Vec3::setY>("y");
cls.prop<&Vec3::getZ, &Vec3::setZ>("z");

The result from above:

Equivalent Wren code for both using .var<&field>("name") or .prop<&getter, &setter>("name"):

// Autogenerated
foreign class Vec3 {
    construct new () {}
    
    foreign x
    foreign x=(rhs)

    foreign y
    foreign y=(rhs)

    foreign z
    foreign z=(rhs)
}

And then simply use it in Wren as:

import "mymodule" for Vec3

var v = Vec3.new()
v.x = 1.23
v.y = 0.0
v.z = 42.42

6.5.3 Read-only class variables #

To bind read-only variables you can use varReadonly function. This won’t define a Wren setter and therefore the variable can be only read.

class Vec3 {
public:
    Vec3(float x, float y, float z) {...}

    const float x;
    const float y;
    const float z;
};

wren::VM vm;
auto& m = vm.module("mymodule");

// Add class "Vec3"
auto& cls = m.klass<Vec3>("Vec3");
cls.ctor<>();
cls.varReadonly<&Vec3::x>("x");
cls.varReadonly<&Vec3::y>("y");
cls.varReadonly<&Vec3::z>("z");

Equivalent Wren code:

// Autogenerated
foreign class Vec3 {
    construct new () {}
    
    foreign x

    foreign y

    foreign z
}

And then simply use it in Wren as:

import "mymodule" for Vec3

var v = Vec3.new(1.1, 2.3, 3.3)
System.print("X value is: %(v.x)") // ok
v.x = 1.23 // error

For read-only properties, you can use propReadonly as shown below:

// Add class "Vec3"
auto& cls = m.klass<Vec3>("Vec3");
cls.ctor<>();
cls.propReadonly<&Vec3::getX>("x");
cls.propReadonly<&Vec3::getY>("y");
cls.propReadonly<&Vec3::getZ>("z");

6.5.4 Class variables via external properties #

Sometimes the property simply does not exist in the C++ class you want to use. So, you somehow need to add this into Wren without changing the original class code. One way to do it is through “external” functions. This is simply a function that is static and must accept the first parameter as a reference to the class instance.

static float getVec3X(Vec3& self) {
    return self.x;
}

static float getVec3Y(Vec3& self) { ... }
static float getVec3Z(Vec3& self) { ... }

// Add class "Vec3"
auto& cls = m.klass<Vec3>("Vec3");
cls.ctor<>();
cls.propExtReadonly<&getVec3X>("x");
cls.propExtReadonly<&getVec3Y>("y");
cls.propExtReadonly<&getVec3Z>("z");

6.5.5 Class variables from base class #

You can bind class variables that belong to the base class, example below:

struct Message {
    uint64_t id = 0;
};

struct MessageText : Message {
    std::string text;
};

wren::VM vm;
auto& m = vm.module("mymodule");

auto& cls = m.klass<MessageText>();
cls.ctor<>();
cls.var<&Message::id>("id");
cls.var<&MessageText::id>("text");

You do not need to bind the base class in order to use its fields in the derived class. This also works with properties cls.prop<>() and cls.propReadonly<>() but does not work with cls.propExt<>() and cls.propReadonlyExt<>(). You can then access both base class and derived class variables in Wren:

import "mymodule" for MessageText

var msg = MessageText.new()
msg.id = 123
msg.text = "Hello World"

6.5.6 Class static variables #

Static variables in Wren are not supported. However, you could cheat a bit with the following code:

class ApplicationGlobals {
public:
    std::string APP_NAME = "Lorem Ipsum Donor";
};

int main() {
    ...

    wren::VM vm(...);
    auto& m = vm.module("mymodule");

    auto& cls = m.klass<ApplicationGlobals>("ApplicationGlobals");
    cls.var<&ApplicationGlobals::APP_NAME>("APP_NAME");
    m.append("var Globals = ApplicationGlobals.new()\n");

    return 0;
}

Wren code:

import "mymodule" for Globals

print("Name: %(Globals.APP_NAME)")

What does m.append do? It allows you to add arbitraty Wren code into the auto generated Wren code from your C++ classes. Anything you will put into the append function will appear at the bottom of the autogenerated code. Calling the append function multiple times is allowed, it will not override previous append call. In this case above, the ApplicationGlobals is created as an instance named Globals. So, from the user’s perspective in the Wren code, it appears as a static member variable. The name must start with a capital letter, otherwise Wren will not allow you to import that variable.

6.6. Upcasting #

Upcasting is when you have a derived class Enemy and you would like to upcast it to Entity. An Enemy class is an Entity, but not the other way around. Remember, upcasting is getting the base class!

This might be a problem when, for example, you have created a derived class inside of the Wren and you are passing it into some C++ function that accepts the base class. What you have to do is to tell the Wren what base classes it can be upcasted to. Consider the following example:

class Entity {
    void update();
};

class Enemy: public Entity {
    ...
};

class EntityManager {
    void add(std::shared_ptr<Entity> entity) {
        entities.push_back(entity);
    }
};

Wren::VM vm;
auto& m = vm.module("game");

// Class Entity
auto& entityCls = m.klass<Entity>("Entity");
entityCls.func<&Entity::update>("update");

// Class Enemy
auto& enemyCls = m.klass<Enemy, Entity>("Enemy");
// Classes won't automatically inherit functions and properties
// therefore you will have to explicitly add them for each
// derived class!
enemyCls.func<&Enemy::update>("update");

// Class EntityManager
auto& mngClass =m.klass<EntityManager>("EntityManager");
mngClass.func<&EntityManager::add>("add");

Notice how we are adding two classes here as template parameters (m.klass<Enemy, Entity>). The first template parameter is the class you are binding, the second (and the next after that) template parameters are the classes for upcasting. You can use multiple classes as the base classes (m.klass<Enemy, Entity, Object>), but that is only recommended if you know what you are doing. Make sure that you will also bind any inherited member functions to the derived class because this is not done automatically.

And the Wren code:

import "game" for EntityManager, Enemy

var manager = ...

var e = Enemy.new()
e.update()
manager.add(e) // ok

Note

Upcasting such as this only works when you want to accept a reference, pointer, or a shared_ptr of the base class. This won’t work with plain value types.

6.7. Class methods that throw #

You can catch the exception (and the exception message) inside of your Wren code. Consider the following C++ class that throws:

class MyCppClass {
public:
    ...
    void someMethod() {
        throw std::runtime_error("hello");
    }
};

And this is how you catch that exception in Wren:

var fiber = Fiber.new { 
    var i = MyCppClass.new()
    i.someMethod() // C++ throws "hello"
}

var error = fiber.try()
System.print("Caught error: " + error) // Prints "Caught error: hello"

6.8. Inheritance #

Wren does not support inheritacne of foreign classes, but there is a workaround. Consider the following C++ class:

class Entity {
public:
    Entity() { ... }
    virtual ~Entity() { ... }

    virtual void update() = 0;
};

Now we want to have our own class in Wren:

import "game" for Entity

class Enemy is Entity { // Not allowed by Wren :(
    construct new (...) {

    }

    update() {
        // Do something specific for Entity class
    }
}

This does not work. You can’t inherit from a foreign class. But, don’t lose hope yet, there is a workaround. First, we need to create a C++ derived class of the base abstract class that overrides the update method. This is necessary so that we can call the proper Wren functions.

class WrenEntity: public Entity {
public:
    // Pass the Wren class to the constructor
    WrenEntity(wren::Variable derived) {
        // Find all of the methods you want to "override"
        // The number of arguments (_) or (_, _) does matter!
        updateFn = derived.func("update(_)");
    }

    virtual ~WrenEntity() {

    }

    void update() override {
        // Call the overriden Wren methods from
        // the Wren class whenever you need to.
        // Pass this class as the base class
        updateFn(this);
    }

private:
    // Store the Wren methods as class fields
    wren::Method updateFn;
};

wren::VM vm;
auto& m = vm.module("game");
auto& cls = m.klass<WrenEntity>("Entity");
cls.ctor<wren::Variable>();

vm.runFromSource("main", ...);

// Call the main function (see Wren code below)
auto res = vm.find("main", "Main").func("main()");

// Get the instance with Wren's specific functions
std::shared_ptr<WrenEntity> enemy = res.shared<WrenEntity>();

And the following Wren code to be used with the code above:

import "game" for Entity

class Enemy {
    update (self) {
        // self points to the base class!
    }
}

class Main {
    static main () {
        // Pass our custom Enemy class
        // to the base Entity class into the constructor.
        // The base class will call the necessary functions.
        return Entity.new(Enemy.new())
    }
}