Wings

An embeddable header-only Python interpreter for C/C++.

GitHub | Documentation | Online REPL

Wings

There are many situations where you may want to add scripting into your program.

The usual options are to embed a Lua/Python interpreter, or to create your own custom scripting language. However, Python is not always easy to set up, and custom scripting is a huge task. Lua would be perfect except that indices start at 1.

Wings focuses primarily on being easy to set up. It does not focus on performance, low memory usage, nor security. Do not execute untrusted scripts! Wings is perfect for configuration files, and light modding.

Compiling

Prerequisites

Embedding

Use git clone, git submodule add, or manually download wings.h and add it to your project.

In exactly one .cpp file, enable the implementation by writing #define WINGS_IMPL before #include "wings.h". Preferably, this is done in a file that doesn't change often.

That's it! The entire API is contained within wings.h so you can just include that wherever you need it. No other files are needed.

Standalone Interpreter

Run the following commands in a terminal. The executable will be located at ./out/bin/wings.

git clone https://github.com/kevlu123/wings
cd wings
cmake -S. -Bout -DCMAKE_BUILD_TYPE=Release -DWINGS_BUILD_SHELL=ON
cmake --build out

Quick Start

The following example prints "Hello, World!" from Python.

#include <stdio.h>

// Define this macro in one C++ source file
// before including wings.h
#define WINGS_IMPL
#include "wings.h"

int main() {
    // Create a context with the default configuration.
    // The context contains the state of the interpreter.
    Wg_Context* ctx = Wg_CreateContext(NULL);

    // Run some Python code.
    if (!Wg_Execute(ctx, "print('Hello, World!')", NULL)) {
        printf("Error: %s\n", Wg_GetErrorMessage(ctx));
    }

    Wg_DestroyContext(ctx);
    return 0;
}
Output
Hello, World!

Manipulating Python Values From C/C++

Since Python is garbage collected, care must be taken to ensure that the interpreter does not free an object that is still being used by C/C++. This is done by managing a reference count on the object.

The following example retrieves two values from Python, adds them together from C/C++, and prints out the result.

// Set some variables in Python.
// Note: Unlike in Python, tuples must be written in brackets.
if (!Wg_Execute(ctx, "(x, y) = (3, 4)", NULL)) {
    // Handle error
}

// Get the global variables
Wg_Obj* x = Wg_GetGlobal(ctx, "x");
Wg_Obj* y = Wg_GetGlobal(ctx, "y");
if (x == NULL || y == NULL) {
    // Handle error
}

// Increase reference counts to prevent the values
// from being garbage collected.
Wg_IncRef(x);
Wg_IncRef(y);

// Add the values together
Wg_Obj* z = Wg_BinaryOp(WG_BOP_ADD, x, y);
if (z == NULL) {
    // Handle error
}

// Decrease reference counts to allow the values
// to be garbage collected again.
// The Wg_XXX functions called later in this example
// will not trigger the garbage collector because
// they do not create any new Python objects, so we can
// safely decrease the reference counts now.
Wg_DecRef(x);
Wg_DecRef(y);

// Get the integer from the Python objects.
// Wg_int is a typedef for int64_t.
if (!Wg_IsInt(x) || !Wg_IsInt(y) || !Wg_IsInt(z)) {
    // Handle unexpected type
}
Wg_int xv = Wg_GetInt(x);
Wg_int yv = Wg_GetInt(y);
Wg_int zv = Wg_GetInt(z);

// Print the result
printf("%lld + %lld = %lld\n", xv, yv, zv);
Output
3 + 4 = 7

Error Handling

Many operations in Wings can fail. Even a simple addition may call an overloaded operator, which may throw an exception.

In Wings, all errors are reported by Python exceptions and the exception or error message can be retrieved via Wg_GetException() or Wg_GetErrorMessage() respectively.

When an exception occurs, you should either handle it or propagate it to the caller. To handle the exception, call Wg_ClearException().

if (Wg_ExecuteExpression(ctx, "1 / 0", NULL) == NULL) {
    // Handle error
    printf("%s", Wg_GetErrorMessage(ctx));
    Wg_ClearException(ctx);
}
Output
Traceback (most recent call last):
  Module __main__, Line 1, Function <string>()
    1 / 0
  Module __builtins__, Function __truediv__()
ZeroDivisionError

Calling Native Code from Python

Calling a C/C++ function from Python is as simple as calling Wg_NewFunction to create a function object, and assigning the result to a Python variable.

The following example embeds a C/C++ function to allow Python to import modules by string. This operation is not normally possible in vanilla Wings.

Wg_Obj* ImportByString(Wg_Context* ctx, Wg_Obj** argv, int argc) {
    // Raise an exception if arguments are invalid.
    // Note that arguments are exempt from garbage collection
    // for the duration of this function.
    if (argc != 1 || Wg_IsString(argv[0])) {
        Wg_RaiseException(ctx, WG_EXC_TYPEERROR, "Expected a string");
        return NULL; // Return NULL to indicate an error
    }
    
    const char* moduleName = Wg_GetString(argv[0]);
    if (Wg_ImportModule(ctx, moduleName, NULL) == NULL) {
        // An exception is already set by Wg_ImportModule
        // so we can just return NULL to propagate the exception.
        return NULL;
    }

    // This is the return value of the function.
    return Wg_None(ctx);
}

void BindAndRunNativeFunction(Wg_Context* ctx) {
    // Create a function object
    Wg_Obj* importByString = Wg_NewFunction(ctx, ImportByString, NULL, NULL);
    if (importByString == NULL) {
        // Handle error
    }

    // Bind the function to a Python variable
    Wg_SetGlobal(ctx, "import_by_string", importByString);

    // Call the function from Python
    const char* code =
        "import_by_string('math')\n"
        "print(math.pi)";
    if (!Wg_Execute(ctx, code, NULL)) {
        // Handle error
    }
}
Output
3.141593

Storing Native Data in Python

Wings allows you to store native data in Python objects. This is useful for storing data that is not easily represented in Python, such as pointers to C/C++ objects.

The following example stores a pointer to a struct in a Python object and gets the pointer back in another function.

struct Data {
    int x;
    int y;
};

// The __init__ method of the Python Data class
Wg_Obj* DataInit(Wg_Context* ctx, Wg_Obj** argv, int argc) {
    if (argc != 1) {
        // Raise error
    }

    // Allocate and initialize a Data struct
    Data* data = (Data*)malloc(sizeof(Data));
    if (data == NULL) {
        // Handle error
    }
    data->x = 3;
    data->y = 4;
    
    // Store the pointer in self
    Wg_SetUserdata(argv[0], data);

    // Register a cleanup function to be
    // called when the object is garbage collected.
    // data will be passed as an argument to free.
    Wg_RegisterFinalizer(argv[0], free, data);

    // Since this is the __init__ function, we return None
    // as we usually do (although implicitly) in Python.
    return Wg_None(ctx);
}

Wg_Obj* PrintData(Wg_Context* ctx, Wg_Obj** argv, int argc) {
    if (argc != 1) {
        // Raise error
    }

    // Validate that the argument is a Data object.
    // The second argument is the name of the class
    // given to Wg_NewClass.
    void* userdata = NULL;
    if (!Wg_GetUserdata(argv[0], "Data", &userdata)) {
        // Handle error
    }

    // Print the data
    Data* data = (Data*)userdata;
    printf("(x = %d, y = %d)", data->x, data->y);

    return Wg_None(ctx);
}

void RunExample(Wg_Context* ctx) {
    // Embed the PrintData function
    Wg_Obj* printData = Wg_NewFunction(ctx, PrintData, NULL, NULL);
    if (printData == NULL) {
        // Handle error
    }
    Wg_SetGlobal(ctx, "print_data", printData);

    // Create a Python class to hold the Data struct
    Wg_Obj* dataClass = Wg_NewClass(ctx, "Data", NULL, NULL);
    if (dataClass == NULL) {
        // Handle error
    }
    Wg_SetGlobal(ctx, "Data", dataClass);

    // Bind the __init__ method to the class
    if (Wg_BindMethod(dataClass, "__init__", DataInit, NULL) == NULL) {
        // Handle error
    }
    
    // Call the print_data function
    const char* code =
        "data = Data()\n"
        "print_data(data)";
    if (!Wg_Execute(ctx, code, NULL)) {
        // Handle error
    }
}
Output
(x = 3, y = 4)