arturocepeda / Cflat

Embeddable lightweight scripting language with C++ syntax
55 stars 8 forks source link

Cflat

Embeddable lightweight scripting language with C++ syntax

Embeddable scripting languages provide the possibility of implementing and tweaking features during the software development process without the need of recompiling or restarting after each code change. This is particularly useful when working with large codebases which take long to compile.

Unfortunately, such an advantage usually implies some additional runtime costs, like a slow execution time (at least in comparison with what would be an equivalent compiled version of the same code written in C++) and a big amount of heap allocations, which might be considerable in performance-critical software like videogames.

Cflat is an embeddable scripting language whose syntax is 100% compatible with C++, what means that all the scripts written for Cflat can be actually compiled in release builds along with the rest of the code.

In this case, compile means compile - the scripts are not compiled into some kind of bytecode, though: they are compiled into machine code for the specific platform you are targeting, just like the rest of the C++ code from the project.

Both software engineers and other members of the team can benefit from Cflat. Regarding the second group, one might say that C++ is not the best choice for developers who are not software engineers or programmers, and C++ is indeed not as friendly as other scripting languages, but the truth is that high-level C++ code, which is the kind of code you usually write in scripts, does not look that different from other widely used languages like C# or Java(Script).

Cflat does not intend to be a fully featured C++ interpreter, but rather a lightweight scripting language whose syntax is 100% compatible with C++. This means that:

In case you are looking for a proper C++ interpreter, you might want to take a look at the following alternatives:

Or, if what you need is a way of getting your C++ code hot-reloaded and recompiled, there are also some good options out there:

FAQ

Is any C++ code compatible with Cflat?

Is any Cflat code compatible with C++?

Is it in Cflat's roadmap to eventually support all the features from C++?

Is Cflat going to provide any extra features outside from C++ in the future?

Does Cflat require any external dependencies?

Is Cflat cross-platform?

Documentation

Getting started

To integrate Cflat in a project, you just have to add the Cflat.cpp file to it and make sure that the header files from the Cflat directory are accessible. In order to take advantage of Cflat, you will need a Cflat environment:

#include "Cflat/Cflat.h"

// ...

Cflat::Environment env;

You can then register the functions and types you would like to have exposed for scripting. Note that Cflat environments are empty by default, without anything registered apart from the built-in types, so you can decide exactly what you need to expose. The Cflat.h header provides you with a bunch of convenience macros to do that:

{
   CflatRegisterFunctionReturnParams1(&env, float, floor, float);
   CflatRegisterFunctionReturnParams1(&env, float, sqrtf, float);
   CflatRegisterFunctionReturnParams2(&env, float, powf, float, float);
}
struct TestStruct
{
   int var1;
   int var2;
};

{
   CflatRegisterStruct(&env, TestStruct);
   CflatStructAddMember(&env, TestStruct, int, var1);
   CflatStructAddMember(&env, TestStruct, int, var2);
}
enum TestEnum
{
   kFirstValue,
   kSecondValue
};

{
   CflatRegisterEnum(&env, TestEnum);
   CflatEnumAddValue(&env, TestEnum, kFirstValue);
   CflatEnumAddValue(&env, TestEnum, kSecondValue);
}
enum class TestEnum
{
   kFirstValue,
   kSecondValue
};

{
   CflatRegisterEnumClass(&env, TestEnum);
   CflatEnumClassAddValue(&env, TestEnum, kFirstValue);
   CflatEnumClassAddValue(&env, TestEnum, kSecondValue);
}
struct Base
{
   int baseMember;
};
struct Derived : Base
{
   int derivedMember;
};

{
   CflatRegisterStruct(&env, Base);
   CflatStructAddMember(&env, Base, int, baseMember);
}
{
   CflatRegisterStruct(&env, Derived);
   CflatStructAddBaseType(&env, Derived, Base);
   CflatStructAddMember(&env, Derived, int, derivedMember);
}
struct BaseA
{
   int baseAMember;
};
struct BaseB
{
   int baseBMember;
};
struct Derived : BaseA, BaseB
{
   int derivedMember;
};

{
   CflatRegisterStruct(&env, BaseA);
   CflatStructAddMember(&env, BaseA, int, baseAMember);
}
{
   CflatRegisterStruct(&env, BaseB);
   CflatStructAddMember(&env, BaseB, int, baseBMember);
}
{
   CflatRegisterStruct(&env, Derived);
   CflatStructAddBaseType(&env, Derived, BaseA);
   CflatStructAddBaseType(&env, Derived, BaseB);
   CflatStructAddMember(&env, Derived, int, derivedMember);
}
struct OuterType
{
   struct InnerType
   {
      int value;
   };
   enum InnerEnum
   {
      kInnerEnumValue
   };
};

{
   CflatRegisterStruct(&env, OuterType);
}
{
   CflatRegisterNestedStruct(&env, OuterType, InnerType);
   CflatStructAddMember(&env, InnerType, int, value);
}
{
   CflatRegisterNestedEnum(&env, OuterType, InnerEnum);
   CflatNestedEnumAddValue(&env, OuterType, InnerEnum, kInnerEnumValue);
}
struct TestStruct
{
   TestStruct();
   ~TestStruct();

   void method(int pValue);
   int constMethod(int pValue) const;

   static void staticMethod(int pValue);
};

{
   CflatRegisterStruct(&env, TestStruct);
   CflatStructAddConstructor(&env, TestStruct);
   CflatStructAddDestructor(&env, TestStruct);
   CflatStructAddMethodVoidParams1(&env, TestStruct, void, method, int);
   CflatStructAddMethodReturnParams1(&env, TestStruct, int, constMethod, int) CflatMethodConst;
   CflatStructAddStaticMethodVoidParams1(&env, TestStruct, void, staticMethod, int);
}

The first argument for the macros can be both a pointer to the environment, or a pointer to the namespace where the type to register is defined:

namespace Math
{
   struct Vector3
   {
      float x;
      float y;
      float z;
   };
}

{
   using namespace Math;
   Cflat::Namespace* ns = env.requestNamespace("Math");

   {
      CflatRegisterStruct(ns, Vector3);
      CflatStructAddMember(ns, Vector3, float, x);
      CflatStructAddMember(ns, Vector3, float, y);
      CflatStructAddMember(ns, Vector3, float, z);
   }
}

Note that bindings to default constructors are not generated automatically, but they need to be added explicitly by using the CflatAddConstructor macro:

{
   CflatRegisterStruct(ns, Vector3);
   CflatAddConstructor(ns, Vector3);
   // ...
}

If return by value is required for registered structs and classes, the copy constructor must be added for them (whether there is a custom implementation or not):

{
   CflatRegisterStruct(ns, Vector3);
   CflatAddCopyConstructor(ns, Vector3);
   // ...
}

Overloaded methods or functions must be registered once per overload:

class TestClass
{
public:
   void method(int pValue);
   void method(float pValue);
};

{
   CflatRegisterClass(&env, TestClass);
   CflatClassAddMethodVoidParams1(&env, TestClass, void, method, int);
   CflatClassAddMethodVoidParams1(&env, TestClass, void, method, float);
}

Methods and functions with default arguments must be registered once per number of parameters:

class TestClass
{
public:
   void method(int pValue1 = 0, int pValue2 = 0);
};

{
   CflatRegisterClass(&env, TestClass);
   CflatClassAddMethodVoid(&env, TestClass, void, method);
   CflatClassAddMethodVoidParams1(&env, TestClass, void, method, int);
   CflatClassAddMethodVoidParams2(&env, TestClass, void, method, int, int);
}

Operators can be registered as regular methods or functions, with the name being operator concatenated with the operator itself, without any spaces in between:

struct TestStruct
{
   const TestStruct operator+(int pValue) const;
};

{
   CflatRegisterStruct(&env, TestStruct);
   CflatStructAddMethodReturnParams1(&env, TestStruct, const TestStruct, operator+, int);
}

For more complex standard types and global values, you can take advantage of the helpers included in CflatHelper.h:

#include "Cflat/CflatHelper.h"

// ...

Cflat::Helper::registerStdString(&env);  // std::string
Cflat::Helper::registerStdOut(&env);     // std::cout

CflatRegisterSTLVector(&env, int);    // std::vector<int>
CflatRegisterSTLVector(&env, float);  // std::vector<float>

CflatRegisterSTLMap(&env, int, float);  // std::map<int, float>

In case you need to register template structs or classes, you can take the way the helper registers STL types as a reference.

Loading scripts into the environment

It is possible to load scripts into the environment both passing the code as a string and passing the path of the file:

env.load("test", "const char* str = \"Hello world!\";");
env.load("./scripts/test.cpp");

Accessing script values and executing script functions

//
//  Cflat script
//
namespace CfTest
{
   static const float kTestConst = 42.0f;

   static void voidFunc(int pA, int pB)
   {
      // ...
   }
   static int returnFunc(int pA, int pB)
   {
      // ...
   }
}
//
//  cpp file
//
Cflat::Value* testConstValue = env.getVariable("CfTest::kTestConst");
const float testConst = CflatValueAs(testConstValue, float);

const int a = 42;
const int b = 10;

Cflat::Function* voidFunc = env.getFunction("CfTest::voidFunc");
env.voidFunctionCall(voidFunc, &a, &b);

Cflat::Function* returnFunc = env.getFunction("CfTest::returnFunc");
const int returnValue = env.returnFunctionCall<int>(returnFunc, &a, &b);

Switching between interpreter and compiler

Cflat comes with the CflatGlobal.h header file, which provides a convenient way to write code that takes advantage of the Cflat scripting system in development configurations and gets the scripts compiled into machine code in final configurations. The first thing to do is to open the CflatGlobalConfig.h header and set both the path where the Cflat includes are, and the path where the scripts are:

#if defined CFLAT_ENABLED

// Relative directory where the Cflat headers are located
# define CflatHeadersPath  ./Cflat

#else

// Relative directory where the scripts are located
# define CflatScriptsPath  ./scripts

#endif

Note that the base directory used as the reference for those relative paths must be defined in the list of additional include directories for the compiler. Otherwise, the #include CflatScript(...) directives (see below) will not compile, since the macro is resolved using brackets (<...>) instead of quotes ("...").

Making use of CflatGlobal.h requires that you implement the following functions (it can be in any cpp of the project):

#if defined CFLAT_ENABLED
namespace CflatGlobal
{
   Cflat::Environment gEnv;
   std::mutex gMutex;

   Cflat::Environment* getEnvironment()
   {
      return &gEnv;
   }
   void lockEnvironment()
   {
      gMutex.lock();
   }
   void unlockEnvironment()
   {
      gMutex.unlock();
   }
   void onError(const char* pErrorMessage)
   {
      std::cerr << "[Cflat] " << pErrorMessage << std::endl;
   }
}
#endif

Then you have to make sure that the source files that need to access scripts include CflatGlobal.h, whether directly in the source file itself, or through some kind of common header which all source files of the project include:

#include "Cflat/CflatGlobal.h"

In order to define in what configurations the scripts should be interpreted through the Cflat environment, you need to make sure that CFLAT_ENABLED is one of the preprocessor definitions in those configurations. In all the configurations where CFLAT_ENABLED is not defined, the scripts will be compiled into machine code. The next step is to include the script or scripts you want to access from the source file:

#include CflatScript(test.cpp)

Once that's done, you can already retrieve values and call functions from the included script.

//
//  Cflat script
//
namespace CfTest
{
   static const float kTestConst = 42.0f;

   static int add(int pA, int pB)
   {
      return pA + pB;
   }
}
//
//  cpp file
//
#include CflatScript(test.cpp)

// ...

const float testConst = CflatGet(float, CfTest::kTestConst);
std::cout << “kTestConst: “ << testConst << std::endl;

const int a = 42;
const int b = 10;
int addTest;
CflatReturnCall(addTest, int, CfTest::add, CflatArg(a), CflatArg(b));

std::cout << “addTest: “ << addTest << std::endl;

Regarding function calls, note that there are two different macros defined in CflatGlobal.h, depending on whether the function to call returns something or not (CflatReturnCall and CflatVoidCall, respectively), and that you have to use the CflatArg macro for each argument.

Using a custom allocator

You can define custom functions both for allocating and for releasing dynamic memory as follows:

Cflat::Memory::malloc = [](size_t pSize) -> void*
{
   return myCustomAllocatorMalloc(pSize);
};
Cflat::Memory::free = [](void* pPtr)
{
   myCustomAllocatorFree(pPtr);
};

NOTE - if you use a custom allocator, remember to release the identifier names registry before shutting down the application (this is not required otherwise):

Cflat::Identifier::releaseNamesRegistry();

Execution hook

There is the possibility of registering an execution hook, for example to implement script debugging features in your application:

void executionHook(Cflat::Environment* pEnv, const Cflat::CallStack& pCallStack)
{
   // ...
}

env.setExecutionHook(executionHook);

The function is then called right before each statement is executed. The evaluateExpression method, provided by the environment, allows you to inspect and modify values.

Support the project

I work on this project in my spare time. If you would like to support it, you can buy me a coffee!

License

Cflat is distributed with a zlib license, and is free to use for both non-commercial and commercial projects:

Copyright (c) 2019-2023 Arturo Cepeda Pérez

This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
   claim that you wrote the original software. If you use this software
   in a product, an acknowledgment in the product documentation would be
   appreciated but is not required.

2. Altered source versions must be plainly marked as such, and must not be
   misrepresented as being the original software.

3. This notice may not be removed or altered from any source distribution.