Native Extensions

Extensions are dynamically-loaded C++ libraries that add functionality to Quartz at runtime. This is how the entire standard library is implemented, and you can create your own extensions.

Overview

Quartz's extension system gives you:

Architecture

When Quartz starts, it:

  1. Scans build/extensions/ for shared libraries (.so on Linux, .dylib on macOS)
  2. Loads each library with dlopen()
  3. Calls the init_extension() function in each library
  4. Extensions register their functions with the FunctionRegistry
build/extensions/
├── system_io/
│   └── libsystem_io.so
├── system_math/
│   └── libsystem_math.so
├── system_string/
│   └── libsystem_string.so
└── your_extension/
└── libyour_extension.so

Creating an Extension

Step 1: Create Directory Structure

mkdir extensions/my_extension
cd extensions/my_extension

Step 2: Create CMakeLists.txt

# extensions/my_extension/CMakeLists.txt

file(GLOB SOURCES *.cpp)

set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "${CMAKE_BINARY_DIR}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=default")

add_library(my_extension SHARED ${SOURCES})

target_include_directories(my_extension PRIVATE 
../../include/core 
${CMAKE_CURRENT_SOURCE_DIR}
)

target_link_libraries(my_extension PRIVATE qz-core)

Step 3: Create Extension Source

// extensions/my_extension/my_extension.cpp

#include "function_registry.h"
#include "types.h"
#include <iostream>
#include <cmath>

// Forward declarations
void register_my_functions(FunctionRegistry& reg);

// Entry point - called by Quartz runtime
extern "C" __attribute__((visibility("default"))) 
void init_extension(FunctionRegistry& reg) {
register_my_functions(reg);
}

void register_my_functions(FunctionRegistry& reg) {
// Simple function with no arguments
auto hello = [](const std::vector<Value>& args) -> Value {
std::cout << "Hello from my extension!" << std::endl;
return Value();  // void return
};
reg.registerFunction("my.hello", hello);

// Function with arguments and return value
auto add = [](const std::vector<Value>& args) -> Value {
if (args.size() < 2) return Value(0);

// Extract integers from arguments
if (std::holds_alternative<int>(args[0]) && 
std::holds_alternative<int>(args[1])) {
int a = std::get<int>(args[0]);
int b = std::get<int>(args[1]);
return Value(a + b);
}

return Value(0);
};
reg.registerFunction("my.add", add);

// Function working with doubles
auto hypot = [](const std::vector<Value>& args) -> Value {
if (args.size() < 2) return Value(0.0);

double a = 0, b = 0;

// Handle both int and double inputs
if (std::holds_alternative<int>(args[0]))
a = std::get<int>(args[0]);
else if (std::holds_alternative<double>(args[0]))
a = std::get<double>(args[0]);

if (std::holds_alternative<int>(args[1]))
b = std::get<int>(args[1]);
else if (std::holds_alternative<double>(args[1]))
b = std::get<double>(args[1]);

return Value(std::sqrt(a*a + b*b));
};
reg.registerFunction("my.hypot", hypot);

// Function working with strings
auto greet = [](const std::vector<Value>& args) -> Value {
if (args.empty()) return Value(std::string("Hello!"));

if (std::holds_alternative<std::string>(args[0])) {
std::string name = std::get<std::string>(args[0]);
return Value("Hello, " + name + "!");
}

return Value(std::string("Hello!"));
};
reg.registerFunction("my.greet", greet);
}

Step 4: Build the Extension

# From the project root
bash rebuild_core.sh

# Or build just extensions
cd build && make

Step 5: Use in Quartz

import system.io as io;

// Use your extension functions
my.hello();  // "Hello from my extension!"

let sum = my.add(5, 3);
io.out.println("5 + 3 =", sum);  // 8

let h = my.hypot(3.0, 4.0);
io.out.println("Hypotenuse:", h);  // 5.0

let greeting = my.greet("World");
io.out.println(greeting);  // "Hello, World!"

The Value Type

Quartz uses std::variant to represent values:

using Value = std::variant<
int,           // Integer
double,        // Floating point
std::string,   // String
bool           // Boolean
>;

Working with Values

// Check type
if (std::holds_alternative<int>(value)) {
int i = std::get<int>(value);
}

// Create values
Value intVal(42);
Value doubleVal(3.14);
Value stringVal(std::string("hello"));
Value boolVal(true);

// Return void (empty value)
return Value();

Function Registry API

// Register a function
reg.registerFunction("namespace.functionName", 
[](const std::vector<Value>& args) -> Value {
// Implementation
return Value();
}
);

// Function signature
// std::function<Value(const std::vector<Value>&)>

Best Practices

Naming Conventions

Error Handling

auto safeDivide = [](const std::vector<Value>& args) -> Value {
if (args.size() < 2) {
std::cerr << "Error: divide requires 2 arguments" << std::endl;
return Value(0.0);
}

double b = std::get<double>(args[1]);
if (b == 0) {
std::cerr << "Error: division by zero" << std::endl;
return Value(0.0);
}

double a = std::get<double>(args[0]);
return Value(a / b);
};

Type Flexibility

// Accept both int and double
double getNumericArg(const Value& v) {
if (std::holds_alternative<int>(v))
return static_cast<double>(std::get<int>(v));
if (std::holds_alternative<double>(v))
return std::get<double>(v);
return 0.0;
}

Advanced Topics

Using External Libraries

// Link against external libraries in CMakeLists.txt
find_package(OpenSSL REQUIRED)
target_link_libraries(my_extension PRIVATE OpenSSL::SSL)

Thread Safety

#include <mutex>

static std::mutex g_mutex;
static int g_counter = 0;

auto incrementCounter = [](const std::vector<Value>& args) -> Value {
std::lock_guard<std::mutex> lock(g_mutex);
return Value(++g_counter);
};

Metadata File

{
"name": "my_extension",
"version": "1.0.0",
"description": "My custom Quartz extension",
"author": "Your Name",
"functions": [
{
"name": "my.hello",
"description": "Prints a greeting",
"params": [],
"returns": "void"
},
{
"name": "my.add",
"description": "Adds two integers",
"params": ["int a", "int b"],
"returns": "int"
}
]
}

Example Extensions

Study the built-in extensions for more examples: