Userdata

Expose C++ objects to Behl scripts with type-safe userdata.

Table of contents

  1. Overview
  2. Creating Userdata
    1. Step 1: Define Your Type and UID
    2. Step 2: Allocate Userdata
    3. userdata_new(State*, size_t, uint32_t)
  3. Accessing Userdata
    1. to_userdata(State*, int32_t)
    2. check_userdata(State*, int32_t, uint32_t)
    3. userdata_get_uid(State*, int32_t)
    4. is_userdata(State*, int32_t)
  4. Complete Example: File I/O
  5. Metatables and Operators
  6. Polymorphic Userdata
  7. Best Practices
    1. 1. Always Use UIDs
    2. 2. Use check_userdata in API Functions
    3. 3. Use to_userdata in Finalizers
    4. 4. Add __gc Metamethod
    5. 5. Store Metatables in Registry
    6. 6. Document UID Naming
  8. Next Steps

Overview

Userdata allows wrapping C++ objects for use in Behl scripts. Each userdata has a unique identifier (UID) for type safety, preventing accidental misuse of incompatible types.

Key features:

  • Type-safe with UIDs
  • Automatic garbage collection
  • Metatable support for operators
  • Resource cleanup via __gc metamethod

Creating Userdata

Step 1: Define Your Type and UID

struct FileHandle {
    FILE* file;
    bool is_open;
};

// Generate a unique ID for this type (compile-time constant)
constexpr uint32_t FileHandle_UID = behl::make_uid("FileHandle");

The make_uid() function generates a unique 32-bit identifier from a string using FNV-1a hashing. Use consistent names to get the same UID across translation units.

Step 2: Allocate Userdata

userdata_new(State*, size_t, uint32_t)

Allocates userdata of given size with a UID and pushes it onto the stack.

void* ptr = behl::userdata_new(S, sizeof(FileHandle), FileHandle_UID);
FileHandle* handle = static_cast<FileHandle*>(ptr);

// Initialize the data
handle->file = fopen("data.txt", "r");
handle->is_open = true;

// Stack: [userdata]

Accessing Userdata

to_userdata(State*, int32_t)

Returns pointer to userdata, or nullptr if not userdata.

void* ptr = behl::to_userdata(S, -1);
if (ptr != nullptr) {
    FileHandle* handle = static_cast<FileHandle*>(ptr);
    // Use handle...
}

Use case: When you don’t need type validation (e.g., in finalizers).

check_userdata(State*, int32_t, uint32_t)

Type-safe retrieval that validates the UID and throws TypeError if wrong type.

// Throws if not userdata or UID mismatch
FileHandle* handle = static_cast<FileHandle*>(
    behl::check_userdata(S, 0, FileHandle_UID)
);

Error handling:

try {
    void* ptr = behl::check_userdata(S, 0, FileHandle_UID);
    FileHandle* handle = static_cast<FileHandle*>(ptr);
} catch (const behl::TypeError& e) {
    // Handle error: "bad argument #1 (expected userdata, got string)"
}

Use case: Always use in API functions for type safety.

userdata_get_uid(State*, int32_t)

Returns the UID of userdata at the given index, or 0 if not userdata.

uint32_t uid = behl::userdata_get_uid(S, -1);
if (uid == FileHandle_UID) {
    // It's a FileHandle
} else if (uid == Vector2D_UID) {
    // It's a Vector2D
}

is_userdata(State*, int32_t)

Returns true if the value at the given index is userdata (any type).

if (behl::is_userdata(S, -1)) {
    // It's some kind of userdata
}

Complete Example: File I/O

#include <behl/behl.hpp>
#include <cstdio>

// Define the userdata type
struct FileHandle {
    FILE* file;
    bool is_open;
};

constexpr uint32_t FileHandle_UID = behl::make_uid("FileHandle");

// Create a new file handle
static int file_open(behl::State* S) {
    auto filename = behl::check_string(S, 0);
    auto mode = behl::check_string(S, 1);
    
    // Allocate userdata
    void* ptr = behl::userdata_new(S, sizeof(FileHandle), FileHandle_UID);
    FileHandle* handle = static_cast<FileHandle*>(ptr);
    
    // Initialize the data
    handle->file = fopen(std::string(filename).c_str(), 
                        std::string(mode).c_str());
    handle->is_open = (handle->file != nullptr);
    
    if (!handle->is_open) {
        behl::error(S, "Failed to open file");
    }
    
    // Set up finalizer metatable (for automatic cleanup)
    behl::table_new(S);  // Create metatable
    behl::push_cfunction(S, file_finalizer);
    behl::table_rawset_field(S, -2, "__gc");
    behl::set_metatable(S, -2);  // Attach to userdata
    
    return 1;  // Return the userdata
}

// Read from file
static int file_read(behl::State* S) {
    // Type-safe retrieval with UID check
    FileHandle* handle = static_cast<FileHandle*>(
        behl::check_userdata(S, 0, FileHandle_UID)
    );
    
    if (!handle->is_open) {
        behl::error(S, "File is closed");
    }
    
    // Read line
    char buffer[1024];
    if (fgets(buffer, sizeof(buffer), handle->file)) {
        behl::push_string(S, buffer);
        return 1;
    }
    
    behl::push_nil(S);  // EOF
    return 1;
}

// Close file
static int file_close(behl::State* S) {
    FileHandle* handle = static_cast<FileHandle*>(
        behl::check_userdata(S, 0, FileHandle_UID)
    );
    
    if (handle->is_open) {
        fclose(handle->file);
        handle->is_open = false;
    }
    return 0;
}

// Finalizer (called by GC)
static int file_finalizer(behl::State* S) {
    // Use to_userdata since we don't validate in finalizers
    void* ptr = behl::to_userdata(S, 0);
    if (ptr) {
        FileHandle* handle = static_cast<FileHandle*>(ptr);
        if (handle->is_open) {
            fclose(handle->file);
            handle->is_open = false;
        }
    }
    return 0;
}

// Register the API
void register_file_api(behl::State* S) {
    behl::register_function(S, "file_open", file_open);
    behl::register_function(S, "file_read", file_read);
    behl::register_function(S, "file_close", file_close);
}

Usage in Behl:

let f = file_open("data.txt", "r");
let line = file_read(f);
print(line);
file_close(f);  // Or let GC close it automatically

// Type safety:
let t = {};
file_read(t);  // TypeError: bad argument #1 (expected userdata, got table)

Metatables and Operators

Userdata can have metatables for operator overloading and custom behavior:

// Vector2D userdata
struct Vector2D {
    double x, y;
};

constexpr uint32_t Vector2D_UID = behl::make_uid("Vector2D");

// __add metamethod
static int vec2_add(behl::State* S) {
    Vector2D* a = static_cast<Vector2D*>(
        behl::check_userdata(S, 0, Vector2D_UID)
    );
    Vector2D* b = static_cast<Vector2D*>(
        behl::check_userdata(S, 1, Vector2D_UID)
    );
    
    // Create result
    void* ptr = behl::userdata_new(S, sizeof(Vector2D), Vector2D_UID);
    Vector2D* result = static_cast<Vector2D*>(ptr);
    result->x = a->x + b->x;
    result->y = a->y + b->y;
    
    // Copy metatable from first operand
    behl::get_metatable(S, 0);
    behl::set_metatable(S, -2);
    
    return 1;
}

// __tostring metamethod
static int vec2_tostring(behl::State* S) {
    Vector2D* v = static_cast<Vector2D*>(
        behl::check_userdata(S, 0, Vector2D_UID)
    );
    std::string str = behl::format("Vector2D({}, {})", v->x, v->y);
    behl::push_string(S, str);
    return 1;
}

// Create and store metatable
void create_vector2d_metatable(behl::State* S) {
    // Create metatable
    behl::table_new(S);
    
    // __add metamethod
    behl::push_cfunction(S, vec2_add);
    behl::table_rawset_field(S, -2, "__add");
    
    // __tostring metamethod
    behl::push_cfunction(S, vec2_tostring);
    behl::table_rawset_field(S, -2, "__tostring");
    
    // Store in registry for reuse
    behl::table_rawset_field(S, behl::REGISTRY_INDEX, "Vector2D_mt");
}

// Constructor
static int vec2_new(behl::State* S) {
    double x = behl::check_number(S, 0);
    double y = behl::check_number(S, 1);
    
    void* ptr = behl::userdata_new(S, sizeof(Vector2D), Vector2D_UID);
    Vector2D* v = static_cast<Vector2D*>(ptr);
    v->x = x;
    v->y = y;
    
    // Attach metatable
    behl::table_rawget_field(S, behl::REGISTRY_INDEX, "Vector2D_mt");
    behl::set_metatable(S, -2);
    
    return 1;
}

Usage:

let v1 = vec2_new(1.0, 2.0);
let v2 = vec2_new(3.0, 4.0);
let v3 = v1 + v2;  // Calls __add
print(v3);         // Calls __tostring: "Vector2D(4.0, 6.0)"

Polymorphic Userdata

Accept multiple userdata types:

static int accepts_any_userdata(behl::State* S) {
    if (!behl::is_userdata(S, 0)) {
        behl::error(S, "Expected userdata");
    }
    
    uint32_t uid = behl::userdata_get_uid(S, 0);
    
    if (uid == FileHandle_UID) {
        FileHandle* f = static_cast<FileHandle*>(behl::to_userdata(S, 0));
        // Handle FileHandle...
    } else if (uid == Vector2D_UID) {
        Vector2D* v = static_cast<Vector2D*>(behl::to_userdata(S, 0));
        // Handle Vector2D...
    } else {
        behl::error(S, "Unknown userdata type");
    }
    
    return 0;
}

Best Practices

1. Always Use UIDs

Never create userdata without a UID. This prevents type confusion bugs.

// Bad - No type safety
void* ptr = malloc(sizeof(MyType));

// Good - Type-safe with UID
constexpr uint32_t MyType_UID = behl::make_uid("MyType");
void* ptr = behl::userdata_new(S, sizeof(MyType), MyType_UID);

2. Use check_userdata in API Functions

Always validate type and UID, provides clear error messages.

// [GOOD] Type-safe API function
static int my_func(behl::State* S) {
    MyType* obj = static_cast<MyType*>(
        behl::check_userdata(S, 0, MyType_UID)
    );
    // ...
}

3. Use to_userdata in Finalizers

Finalizers should be defensive since they’re called by GC.

static int my_finalizer(behl::State* S) {
    void* ptr = behl::to_userdata(S, 0);
    if (ptr) {
        MyType* obj = static_cast<MyType*>(ptr);
        // Cleanup...
    }
    return 0;
}

4. Add __gc Metamethod

Ensures resources are cleaned up even if scripts forget.

behl::table_new(S);
behl::push_cfunction(S, my_finalizer);
behl::table_rawset_field(S, -2, "__gc");
behl::set_metatable(S, -2);

5. Store Metatables in Registry

Reuse metatables for efficiency:

// Create once
behl::table_rawget_field(S, behl::REGISTRY_INDEX, "MyType_mt");
if (behl::is_nil(S, -1)) {
    behl::pop(S, 1);
    behl::table_new(S);
    // Configure metatable...
    behl::table_rawset_field(S, behl::REGISTRY_INDEX, "MyType_mt");
}

6. Document UID Naming

Use consistent naming conventions like "TypeName" for make_uid().

// [GOOD] Consistent naming
constexpr uint32_t FileHandle_UID = behl::make_uid("FileHandle");
constexpr uint32_t Vector2D_UID = behl::make_uid("Vector2D");

// [BAD] Inconsistent - will generate different UIDs
constexpr uint32_t UID1 = behl::make_uid("file_handle");  // Different!
constexpr uint32_t UID2 = behl::make_uid("FileHandle");

Next Steps


Copyright © 2025 behl Project. Distributed under MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.