Giter Site home page Giter Site logo

nelarius / wrenpp Goto Github PK

View Code? Open in Web Editor NEW
97.0 9.0 17.0 292 KB

Minimal, zero dependency C++ binding generator for the Wren programming language

License: MIT License

C++ 70.71% Lua 3.66% Shell 0.25% Makefile 25.38%
wren language-bindings cpp c-plus-plus scripting bindings

wrenpp's Introduction

Wren++

Build Status

A C++ wrapper for the Wren programming language, in the spirit of LuaBridge and Luabind. Both Wren and this library are still under development, so breaking changes will occasionally be introduced.

Wren++ currently provides:

  • A RAII wrapper for the Wren virtual machine
  • Automatic binding code generation for any C++ function and class
  • Convenient access for calling Wren class methods from C++
  • Uses C++14 template metaprogramming -- no macros!

Known issues:

  • Not type-safe. It's undefined what happens when you try to bind code that returns a type which hasn't itself been bound (most likely a crash is going to happen)
  • Wren access from C++ is rather minimal

Table of contents

Build

Clone the repository using git clone https://github.com/nelarius/wrenpp.git. The entire library consists just of Wren++.h and Wren++.cpp, and so the easiest way to use the library is just to include them in your project directly. Just remember to compile with C++14 features turned on!

Alternatively, you can use premake to generate a build file for the static library:

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>

At a glance

Let's fire up an instance of the Wren VM and execute some code:

#include "Wren++.h"

int main() {
  wrenpp::VM 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.

wrenpp::Result res = vm.executeString(
  "// calling nonexistent variable"
  "foobar.call()"
);
if ( res == wrenpp::Result::CompileError ) {
  std::cout << "foobar doesn't exist and a compilation error occurs.\n";
}

There are two other codes: wrenpp::Result::RuntimeError and wrenpp::Result::Success.

Accessing Wren from Cpp

Methods

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 the variadic template method operator(),

wrenpp::VM vm{};
vm.executeModule( "bar" );

wrenpp::Method say = vm.method( "main", "Foo", "say(_)" );
say( "Hello from C++!" );

VM::method has the following signature:

Method VM::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).

The return value of a Wren method can be accessed by doing

wrenpp::Method returnsThree = vm.method("main", "returnsThree", "call()");
wrenpp::Value val = returnsThree();
double val = val.as<double>();

The operator() method on wrenpp::Method returns a wrenpp::Value object, which can be cast to the wanted return type by calling as<T>().

Number, boolean, and string values are stored within wrenpp::Value itself. Note that do to this, you don't want to write

const char* greeting = returnsGreeting().as<const char*>();

because the string value is storesd in the wrenpp::Value instance itself. This would result in trying to dereference a string value which no longer exists. Store it locally before using the string value, like we did above:

wrenpp::Value greeting = returnsGreeting();
printf("%s\n", greeting.as<const char*>());

Accessing Cpp from Wren

Wren++ allows you to bind C++ functions and methods to Wren classes. You provide the VM instance with the name of the foreign method and the corresponding C++ function pointer. These are then looked up by the VM when it encounters a foreign method in source code.

Foreign methods

You can implement a Wren foreign method as a stateless free function in C++. Wren++ offers an easy to use wrapper over the functions. 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 "Wren++.h"
#include <cmath>

int main() {

  wrenpp::VM vm;
  vm.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();

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

Foreign classes

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

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 "Wren++.h"

int main() {
  wrenpp::VM vm;
  vm.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.

Properties

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.

vm.beginModule( "main" )
  .bindClass< Vec3, float, float, float >( "Vec3" )
    .bindGetter< decltype(Vec3::x), &Vec3::x >( "x" )
    .bindSetter< decltype(Vec3::x), &Vec3::x >( "x=(_)" )
    .bindGetter< decltype(Vec3::y), &Vec3::y >( "y" )
    .bindSetter< decltype(Vec3::y), &Vec3::y >( "y=(_)" )
    .bindGetter< decltype(Vec3::z), &Vec3::z >( "z" )
    .bindSetter< decltype(Vec3::z), &Vec3::z >( "z=(_)" );

Getters and setters are implicitly assumed to be non-static methods.

Methods

Using registerMethod allows you to bind a class method to a Wren foreign method. Just do:

#include "Wren++.h"

int main() {
  wrenpp::VM vm;
  vm.beginModule( "main" )
    .bindClass< Vec3, float, float, float >( "Vec3" )
      // properties as before
      .bindGetter< decltype(Vec3::x), &Vec3::x >( "x" )
      .bindSetter< decltype(Vec3::x), &Vec3::x >( "x=(_)" )
      .bindGetter< decltype(Vec3::y), &Vec3::y >( "y" )
      .bindSetter< decltype(Vec3::y), &Vec3::y >( "y=(_)" )
      .bindGetter< decltype(Vec3::z), &Vec3::z >( "z" )
      .bindSetter< decltype(Vec3::z), &Vec3::z >( "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(_) ?

CFunctions

Wren++ 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.

vm.beginModule( "builtin/imgui" )
  .beginClass( "Imgui" )
    // windows & their formatting
    .bindCFunction( true, "begin(_)", &VM::begin )
    .bindFunction< decltype(&ImGui::End), &ImGui::End>( true, "end()" )
    .bindCFunction( true, "sliderFloat(_,_,_,_)", &VM::sliderFloat)
  .endClass();

If you need to access and return foreign object instances within your CFunction, you can use the following two helper functions.

Use wrenpp::getSlotForeign<T>(WrenVM*, int) to get a bound type from the slot API:

void setWindowSize(WrenVM* vm) {
  ImGui::SetNextWindowSize(*(const ImVec2*)wrenpp::getSlotForeign<Vec2i>(vm, 1));
}

Use wrenpp::setSlotForeignValue<T>(WrenVM*, int, const T&) and wrenpp::setSlotForeignPtr<T>(WrenVM*, int, T* obj) to place an object with foreign bytes in a slot, by value and by reference, respectively. wrenpp::setSlotForeignValue<T> uses the type's copy constructor to copy the object into the new value.

Cpp and Wren lifetimes

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.

Customize VM behavior

The following customizations are affect all VMs.

Customize printing

You can provide your own implementation for System.print by assigning a callable object with the signature void(const char*) to wrenpp::VM::writeFn. By default, writeFn is implemented simply as:

WriteFn VM::writeFn = []( const char* text ) -> void { std::cout << text; };

Customize error printing

You can provide your own function to route error messages. Assign a callable object with the signature void(WrenErrorType, const char*, int, const char*) (for the error type, module name, line number, and message, respectively) to wrenpp::VM::errorFn. By default, Wren++ styles the errors to stdout as

WREN_ERROR_COMPILE in main:15 > Error at 'Vec3': Variable is used but not defined.

Customize module loading

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 VM::loadModuleFn a new value, which can be a free standing function, or callable object of type char*( const char* ).

By default, VM::loadModuleFn has the following value.

VM::loadModuleFn = []( const char* mod ) -> char* {
    std::string path( mod );
    path += ".wren";
    auto source = wrenpp::fileToString( path );
    char* buffer = (char*) malloc( source.size() );
    memcpy( buffer, source.c_str(), source.size() );
    return buffer;
};

Customize heap allocation and garbage collection

You can bind your own allocator to Wren by providing the following generic allocation function (which is set to std::realloc by default):

wrenpp::VM::reallocateFn = std::realloc;

reallocateFn needs to be a callable of type void*(void* memory, std::size_t newSize). To allocate memory, memory is null and newSize is the desired size. To free memory, memory is the allocated pointer, and newSize is zero. To grow an existing allocation, memory is the already allocated memory, and newSize is the desired size. The function should return the same pointer if it was able to grow the allocation in place. The new pointer is returned if the allocation was moved. To shrink an allocation, memory is the already allocated pointer, and newSize is the desired size. The same pointer is returned.

The initial heap size is the number of bytes Wren will have allocated before triggering the first garbage collection. By default, it's 10 MiB.

wrenpp::VM::initialHeapSize = 0xA00000u;

After a collection occurs the heap will have shrunk. Wren will allow the heap to grow to (100 + heapGrowthPercent) % of the current heap size before the next collection occurs. By default, the heap growth percentage is 50 %.

wrenpp::VM::heapGrowthPercent = 50;

The minimum heap size is the heap size, in bytes, below which collections will not be carried out. The idea of the minimum heap size is to avoid miniscule heap growth (calculated based on the percentage mentioned previously) and thus very frequent collections. By default, the minimum heap size is 1 MiB.

wrenpp::VM::minHeapSize = 0x100000u;

TODO:

  • 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 Types. One is used for registration, which iterates Type as well. This doesn't work in the case that the user registers different types for multiple Wren instances.
  • 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?

wrenpp's People

Contributors

nathanielhourt avatar nelarius avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wrenpp's Issues

Binding a free function as a non-static member function

I'm trying to bind a free function, as a non-static member function of a foreign class.
Here is my Wren code:

foreign class MyData {
	foreign value
	foreign value=(rhs)

	foreign twiceValue

	toString { "MyData(%(value))" }
}

// Some where else
class Player {
	// ...
	static puts(obj) {
		System.print("Putting: %(obj)")
		System.print("Putting twice: %(obj.twiceValue)")
	}
}

And here is my C++ code (module names are handled correctly):

class MyData
{
public:
	double value;
};

double twiceValue(MyData& self)
{
	return self.value * 2;
}

// *m_VM is a thin wrapper around `wrenpp::WrenVM` which provides `beginModule` as a wrapper around wrenpp`s version of it.
m_VM->beginModule("src/wren/lib/components")
	.bindClass<MyData>("MyData")
		.bindGetter<decltype(MyData::value), &MyData::value>("value")
		.bindSetter<decltype(MyData::value), &MyData::value>("value=(_)")
		.bindMethod<decltype(twiceValue), twiceValue>(false, "twiceValue")
	.endClass()
.endModule();

// Loading modules...

m_VM->GetWrenpp().method("src/wren/test", "Player", "puts(_)")(MyData{12});

When I run it, I get:

Putting: MyData(12)

And then it segfaults.
Using a static function (binding twiceValue as static and passing instance as its first parameter) or a member function works fine.

Can't import modules

I have embedded wren into my program, it mostly works, but problems start to happen when I try to import my module (originally meant for declaring foreign functions, but I realised that the same problem appears also with simple classes).

First I bind classes and methods, then I create the virtual machine (doing it in the other order caused the program to hand indefinitely with no warning), then I import the module (using the import command in wren, I also tried executeModule to no avail), everything seems all right, but when I try to use a constructor from there (even if everything foreign is commented), it tells me that (class name) variable is used but not defined. It appears like if the module wasn't loaded at all, but I have checked that my loadModuleFn loads the correct code (incorrect code reliably causes trouble).

Here is the dummy code I used (it worked fine in the online IDE):
class Vec2 {
setX(x) { _x = x}
setY(Y) { _y = y}
x {_x}
y {_y}
construct new(x, y) {
_x = x
_y = y
}
static print(v) {
System.print("%(v.x) , %(v.y)")
}
}
(sorry, markdown removes indentation) I am running it using the vm::executeString command. In this case, it is:
import "game"
var v = Vec2(1, 1) // Error

[Question] calling c++ function with Fn instance argument

I need pass Fn instance to c++ and call it later. Is it possible please?

for example

test.wren:

class Utils {
  foreign static add_callback(fn)
}

Utils.add_callback(Fn.new { System.print("hello world") })

main.cc:

void add_callback(WrenVM* vm)
{
   // ??
}

int main()
{
  wrenpp::VM vm{};
  vm.beginModule("main")
    .beginClass("Utils")
    .bindCFunction(true, "add_callback(_)", &add_callback)
    .endClass();

  vm.executeModule("test");
  return 0;
}

Wrong information about writing setters

The information about the bindGetter function is wrong in the introduction in README.md. It shows an example that I should use bindGetter< decltype(&Vec3::x), &Vec3::x >( "x" ), while that one doesn't work, it should be bindGetter< decltype(Vec3::x), &Vec3::x >( "x" ), without the ampersand inside the decltype.

Binding a C++ member function to a static Wren method

Is it possible to bind a C++ member function (or closure, for that matter) to a static Wren method? I can't figure out a way to get any sort of instance state inside those function bindings. (For example, if I wanted to bind a Wren function to close a window, I'd need access to the window handle from a closure scope or member variable)

Define the C++ standard

For many, it is easier to compare compilers to the standards they support, and see if a project is compatible.

So, I would like to know, which C++ standard this library adheres to? From what I can see - but I haven't really seen everything... - I would actually believe it is C++99. But again, I am not sure - therefore I ask. :)

Are shared_ptr supported?

Hello,

I played around with wrenpp and its very nice. The only thing I could not figure out was if it supports shared_ptr? We pass around a lot of shared pointers and want to pass them into/out of wren.

Thanks,
David

How can I help and take part in your project?

Hello! I'd like to join your project.
A little about yourself: I'm junior C++ developer, and I don't have commercial development experience. If you have tasks (junior), I'm happy to help project. Sorry with my not very confident English:)

Fix the failing property test

The property tests fail because properties are currently always returned by value, even for foreign types.

Something more clever than just doing WrenSlotAPI::set<T>(...) is required in propertyGetter.

Segfault when calling a wren method from C++

When I try to call a wren method directly from C++, I get a segfault.

I have created a module named game, created in it a class named Cutscene and gave it a method resume() (no arguments). I load the module. When I call:
vm_->executeString("Cutscene.resume()");
It works all right. But when I try to call it without having wren parse it, using code like this:
wrenpp::Method resume = vm_->method("game", "Cutscene", "resume()");
wrenpp::Value val = resume();
I get a segfault at Wren++.h:265, from dereferencing a null pointer.

I have also tried to write main as module main, result was the same. When I wrote the name of a non-existent function, it threw an exception as it should.

[Question] How to use wrenpp to bind foreign hierarchies?

Hi, i'm using wrenpp an a toy game engine and I came to a use case where I'm a bit stuck. Suppose I have a base Node class and a Spatial class that inherits Node. Since in wren it is not possible to subclass a foreign class, in wrenpp I have to bind all the specific base class methods for each derived class each time.
What would be the best approach to automate this in wrenpp?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.