A C++ wrapper for the Wren programming language. As the language itself and this library are both heavily WIP, expect everything in here to change.
Wrenly currently provides:
- a RAII wrapper for the Wren virtual machine
- convenient access to calling Wren class methods from C++
- automatic wrapping code generation for any C++ functions and classes
- template-based -- no macros!
Current defincies:
- not that type-safe. It's pretty undefined what happens when you try to bind code that returns a type which hasn't itself been bound
- Wrenly has no concept of const-ness. When you return a const pointer to one of your instances from a bound function, the resulting foreign object in Wren will happily call non-const methods -- yikes!
- Wren access from C++ is rather minimal
Currently developing against wren:master@139b447
.
Clone the repository using git clone https://github.com/nelarius/wrenly.git
. The easiest way to build the project is to include the contents of the src/
folder in your project, since there's so little code. Just remember to compile with C++14 features turned on!
Alternatively, you can build the static library with premake:
premake5 vs2015 --include=<path to wren.h>
If you want to build the tests as well, then you need to include the location of wren's lib/
folder.
premake5 vs2015 --include=<path to wren.h> --link=<path to wren/lib>
Let's fire up an instance of the Wren VM and execute some code:
#include "Wrenly.h"
int main() {
wrenly::Wren vm{};
vm.executeString( "System.print(\"Hello, world!\")" );
return 0;
}
The virtual machine is held internally in a pointer. Wren
uniquely owns the virtual machine, which means that the Wren
instance can't be copied, but can be moved when needed - just like a unique pointer.
Module names work the same way by default as the module names of the Wren command line module. You specify the location of the module without the .wren
postfix.
vm.executeModule( "script" ); // refers to script.wren
Both executeString
and executeModule
return a code indicating if the interpretation encountered any errors.
wrenly::Result res = wrenly.executeString(
"// calling nonexistent variable"
"foobar.call()"
);
if ( res == wrenly::Result::CompileError ) {
std::cout << "foobar doesn't exist and a compilation error occurs.\n";
}
There are two other codes: Result::RuntimeError
and Result::Success
.
You can use the Wren
instance to get a callable handle for a method implemented in Wren. Given the following class, defined in bar.wren
,
class Foo {
static say( text ) {
System.print( text )
}
}
you can call the static method say
from C++ by using void Method::operator( Args&&... )
,
wrenly::Wren wren{};
wren.executeModule( "bar" );
wrenly::Method say = wren.method( "main", "Foo", "say(_)" );
say( "Hello from C++!" );
Wren::method
has the following signature:
Method Wren::method(
const std::string& module,
const std::string& variable,
const std::string& signature
);
module
will be "main"
, if you're not in an imported module. variable
should contain the variable name of the object that you want to call the method on. Note that you use the class name when the method is static. The signature of the method has to be specified, because Wren supports function overloading by arity (overloading by the number of arguments).
TODO: improve
Wrenly allows you to bind C++ functions and methods to Wren classes. This is done by storing the names or signatures along with the function pointers, so that Wren can find them. That is, you bind all your code once, after which all your Wren instances can use them.
You can implement a Wren foreign method as a stateless free function in C++. Wrenly offers an easy to use wrapper over the functions. Note that only primitive types and std::string
work for now. Support for registered types will be provided later once the custom type registration feature is complete.
Here's how you could implement a simple math library in Wren by binding the C++ standard math library functions.
math.wren:
class Math {
foreign static cos( x )
foreign static sin( x )
foreign static tan( x )
foreign static exp( x )
}
main.cpp:
#include "Wrenly.h"
#include <cmath>
int main() {
wrenly::Wren wren{};
wrenly::beginModule( "math" )
.beginClass( "Math" )
.bindFunction< decltype(&cos), &cos >( true, "cos(_)" )
.bindFunction< decltype(&sin), &sin >( true, "sin(_)" )
.bindFunction< decltype(&tan), &tan >( true, "tan(_)" )
.bindFunction< decltype(&exp), &exp >( true, "exp(_)" )
.endClass()
.endModule();
wren.executeString( "import \"math\" for Math\nSystem.print( Math.cos(0.12345) )" );
return 0;
}
Both the type of the function (in the case of cos
the type is double(double)
, for instance, and could be used instead of decltype(&cos)
) and the reference to the function have to be provided to bindFunction
as template arguments. As arguments, bindFunction
needs to be provided with a boolean which is true, when the foreign method is static, false otherwise. Finally, the method signature is passed.
The free function needs to call functions like
wrenGetArgumentDouble
,wrenGetArgumentString
to access the arguments passed to the method. When you register the free function, Wrenly wraps the free function and generates the appropriatewrenGetArgument*
function calls during compile time. Similarly, if a function returns a value, the call to the appropriatewrenReturn*
function is inserted at compile time.
Free functions don't get us very far if we want there to be some state on a per-object basis. Foreign classes can be registered by using bindClass
on a module context. Let's look at an example. Say we have the following Wren class representing a 3-vector:
foreign class Vec3 {
construct new( x, y, z ) {}
foreign norm()
foreign dot( rhs )
foreign cross( rhs ) // returns the result as a new vector
// accessors
foreign x
foreign x=( rhs )
foreign y
foreign y=( rhs )
foreign z
foreign z=( rhs )
}
/*
* This is a creator object for the C++ API to use in order to create foreign objects
* and return them back to Wren with the required state
*/
var createVector3 = Fn.new { | x, y, z |
return Vec3.new( x, y, z )
}
We would like to implement it using the following C++ struct.
struct Vec3 {
union {
float v[3];
struct { float x, y, z; };
};
Vec3( float x, float y, float z )
: v{ x, y, z }
{}
float norm() const {
return sqrt( x*x + y*y + z*z );
}
float dot( const Vec3& rhs ) const {
return x*rhs.x + y*rhs.y + z*rhs.z;
}
Vec3 cross( const Vec3& rhs ) const {
return Vec3 {
y*rhs.z - z*rhs.y,
z*rhs.x - x*rhs.z,
x*rhs.y - y*rhs.x
};
}
};
Let's start by binding the class to Wren and adding properties.
A class is bound by writing bindClass
instead of beginClass
. This binds the specified constructor and destructor to Wren, and allocates a new instance of your class within Wren.
#include "Wrenly.h"
int main() {
wrenly::Wren wren{};
wrenly::beginModule( "main" )
.bindClass< Vec3, float, float, float >( "Vec3" );
// you can now construct Vec3 in Wren
return 0;
}
Pass the class type, and constructor argument types to bindClass
. Even though a C++ class may have many constructors, only one constructor can be registered with Wren.
If your class or struct has public fields you wish to expose, you can do so by using bindGetter
and bindSetter
. This will automatically generate a function which returns the value of the field to Wren.
wrenly::beginModule( "main" )
.bindClass< Vec3, float, float, float >( "Vec3" )
.bindGetter< decltype(&Vec3::x), &Vec3::x >( false, "x" )
.bindSetter< decltype(&Vec3::x), &Vec3::x >( false, "x=(_)" )
.bindGetter< decltype(&Vec3::y), &Vec3::y >( false, "y" )
.bindSetter< decltype(&Vec3::y), &Vec3::y >( false, "y=(_)" )
.bindGetter< decltype(&Vec3::z), &Vec3::z >( false, "z" )
.bindSetter< decltype(&Vec3::z), &Vec3::z >( false, "z=(_)" );
Using registerMethod
allows you to bind a class method to a Wren foreign method. Just do:
#include "Wrenly.h"
int main() {
wrenly::Wren wren{};
wrenly::beginModule( "main" )
.bindClass< Vec3, float, float, float >( "Vec3" )
// properties
.bindGetter< decltype(&Vec3::x), &Vec3::x >( false, "x" )
.bindSetter< decltype(&Vec3::x), &Vec3::x >( false, "x=(_)" )
.bindGetter< decltype(&Vec3::y), &Vec3::y >( false, "y" )
.bindSetter< decltype(&Vec3::y), &Vec3::y >( false, "y=(_)" )
.bindGetter< decltype(&Vec3::z), &Vec3::z >( false, "z" )
.bindSetter< decltype(&Vec3::z), &Vec3::z >( false, "z=(_)" )
// methods
.bindMethod< decltype(Vec3::norm), &Vec3::norm >( false, "norm()" )
.bindMethod< decltype(Vec3::dot), &Vec3::dot >( false, "dot(_)" )
.endClass()
.endModule();
return 0;
}
The arguments are the same as what you pass bindFunction
, but as the template parameters pass the method type and pointer instead of a function.
We've now implemented two of Vec3
's three foreign functions -- what about the last foreign method, cross(_)
?
Wrenly let's you bind functions of the type WrenForeignMethodFn
, typedefed in wren.h
, directly. They're called CFunctions for brevity (and because of Lua). Sometimes it's convenient to wrap a collection of C++ code manually. This happens when the C++ library interface doesn't match Wren classes that well. Let's take a look at binding the excellent dear imgui library to Wren.
Many functions to ImGui are very overloaded and have lengthy signatures. We would have to fully qualify the function pointers, which would make the automatic bindings a mess. Additionally, many ImGui functions (such as SliderFloat) take in pointers to primitive types, like float, bool. Wren doesn't have any concept of out
parameters, so we will make our Wren ImGui API take in a number by value, and return the new value.
class Imgui {
// windows
foreign static begin( name ) // begin a window scope
foreign static end() // end a window scope
foreign static sliderFloat( label, value, min, max ) // RETURNS the new value
}
First, let's implement wrappers for ImGui::Begin
and ImGui::SliderFloat
with reasonable default values.
void begin(WrenVM* vm) {
ImGui::Begin((const char*)wrenGetSlotString(vm, 1), NULL, 0);
}
void sliderFloat(WrenVM* vm) {
const char* label = wrenGetSlotString(vm, 1);
float value = float(wrenGetSlotDouble(vm, 2));
float min = float(wrenGetSlotDouble(vm, 3));
float max = float(wrenGetSlotDouble(vm, 4));
ImGui::SliderFloat(label, &value, min, max);
wrenSetSlotDouble(vm, 0, value);
}
Here's what the binding code looks like. Note that ImGui::End is trivial to bind as it takes no arguments.
wrenly::beginModule( "builtin/imgui" )
.beginClass( "Imgui" )
// windows & their formatting
.bindCFunction( true, "begin(_)", wren::begin )
.bindFunction< decltype(&ImGui::End), &ImGui::End>( true, "end()" )
.bindCFunction( true, "sliderFloat(_,_,_,_)", wren::sliderFloat)
.endClass();
If you need to access and return foreign object instances within your CFunction, you can use the following two helper functions.
Use wrenly::getForeignSlotPtr<T, int>(WrenVM*)
to get a bound type from the slot API:
void setWindowSize(WrenVM* vm) {
ImGui::SetNextWindowSize(*(const ImVec2*)wrenly::getForeignSlotPtr<Vec2i, 1>(vm));
}
Use wrenly::setForeignSlotValue<T>(WrenVM*, const T&)
and wrenly::setForeignSlotPtr<T>(WrenVM*, T* obj)
to place an object with foreign bytes in slot 0, by value and by reference, respectively. wrenly::setForeignSlotValue<T>
uses the type's copy constructor to copy the object into the new value.
If the return type of a bound method or function is a reference or pointer to an object, then the returned wren object will have C++ lifetime, and Wren will not garbage collect the object pointed to. If an object is returned by value, then a new instance of the object is also constructed withing the returned Wren object. In this situation, the returned Wren object has Wren lifetime and is garbage collected.
The Wren scripting language does not implement System.print( ... )
itself, but expects the embedder to provide it. The Wren
wrapper class does exactly that with the static Wren::writeFn
field, which is of type std::function< void( WrenVM*, const char* ) >
. By default, it is implemented as follows.
Wren::writeFn = []( WrenVM* vm, const char* text ) -> void { printf( text ) };
When the virtual machine encounters an import statement, it executes a callback function which returns the module source for a given module name. If you want to change the way modules are named, or want some kind of custom file interface, you can change the callback function. Just set give Wren::loadModuleFn
a new value, which can be a free standing function, or callable object of type char*( const char* )
.
By default, Wren::loadModuleFn
has the following value.
Wren::loadModuleFn = []( const char* mod ) -> char* {
std::string path( mod );
path += ".wren";
auto source = wrenly::FileToString( path );
char* buffer = (char*) malloc( source.size() );
memcpy( buffer, source.c_str(), source.size() );
return buffer;
};
TODO
- Assert when an object pointer is misaligned
- Return registered foreign classes by value, and by reference
- Add function to get the value returned by the wren method.
- A Value class is needed. Has template methods to get the actual value on the Wren stack.
- A compile-time method must be devised to assert that a type is registered with Wren. Use static assert, so incorrect code isn't even compiled!
- For instance, two separate
Type
s. One is used for registration, which iteratesType
as well. This doesn't work in the case that the user registers different types for multipleWren
instances.
- For instance, two separate
- I need to be able to receive the return value of a foreign method, and return that from
operator()( Args... args ).
- Ideally there would be a wrapper for WrenValue, which might be null.
- There needs to be better error handling for not finding a method.
- Is Wren actually responsible for crashing the program when a method is not found?
- Does Wren actually crash the program when an invalid operator is used on a class instance?
- Make Wren::collectGarbage less verbodes