Native Modules
In Luwow, to create functionality that extends the capabilities of the engine, you can use native modules. Native modules are statically registered binary modules that can be required and used from Luau.
For example, by using the GUI library, you can export GUI functionality to Luau, which allows the user to create native OS components to render:
local Gui = require("@Luwow/gui")
local Window = Gui.createWindow({
Type = "Window",
Title = "Luwow Created Window",
Width = 400,
Height = 300
})
Window.show()
Creating a new Native Module
To begin creating a new native module, we'll create a new folder structure which will host our source files for our module. For the sake of this tutorial, we'll begin with a rather easy idea, a printer module which will print the given string input to the output.
Our folder structure should look like this:
Under the Printer folder, we'll have our module implementation, and under the PrinterRunner folder, we'll have a C++ program which binds the module to the engine for the user to use the module from Luau.
Printer Implementation
Under the Printer folder, we'll create a new header file called PrinterModule.h, which we'll use to define our new module class.
Every module class must inherit from the ILuauModule class found in the ILuauModule.h file within the engine.
PrinterModule.h
#pragma once
#include "ILuauModule.h"
#include <string>
// We'll create a new namespace for our module under Luwow.
namespace Luwow::Printer {
using ILuauModule = Luwow::Engine::ILuauModule;
using Engine = Luwow::Engine::Engine;
class PrinterModule : public ILuauModule { // We inherit from the ILuauModule class.
public:
PrinterModule();
~PrinterModule() = default;
const char* getModuleName() const override; // Gets the module name for requiring.
const char* getModuleAlias() const override; // Gets the module alias for requiring.
const LuauExport* getExports() const override; // Exported functions under this module.
ILuauModule* initialize(Engine* engine) override; // Initializes the module.
void printMessage(std::string message); // Our function to print the given string to the output.
};
}
Next, we'll implement this class under a new C++ file called PrinterModule.cpp:
PrinterModule.cpp
#include "PrinterModule.h"
#include "Engine.h"
#include "lua.h"
#include "lualib.h"
#include <iostream>
#include <string>
namespace Luwow::Printer {
using ILuauModule = Luwow::Engine::ILuauModule;
using Engine = Luwow::Engine::Engine;
// For all methods that require the printer instance, we need to get it from the userdata.
static PrinterModule* getModuleInstance(lua_State* L) {
PrinterModule* printer = static_cast<PrinterModule*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!printer) {
throw std::runtime_error("Printer module not found!");
}
return printer;
}
PrinterModule::PrinterModule() {}
// Initializes a new printer module object. The engine pointer here is unused.
ILuauModule* PrinterModule::initialize(Engine* engine) {
PrinterModule* printer = new PrinterModule();
return printer;
}
// The class method that actually provides the functionality.
void PrinterModule::printMessage(std::string message) {
std::cout << message << "\n"; // We print the given message to the output!
}
// The function that will be called from Luau, when the user calls printer.printMessage().
static int printMessage(lua_State* L) {
PrinterModule* printer = getModuleInstance(L); // We get the module instance from this function.
const char* string = luaL_checkstring(L, 1); // Then we get the string argument from the stack.
printer->printMessage(std::string(string)); // We then call the printMessage method of our printer with the given string.
return 1;
}
// The name that will be used to require the module.
const char* PrinterModule::getModuleName() const {
return "Printer";
}
// The alias that will be used to require the module.
const char* PrinterModule::getModuleAlias() const {
return "Luwow";
}
// The exported functions to Luau.
static LuauExport exports[] = {
{ "printMessage", printMessage },
{ nullptr, nullptr }
};
// Returns the exported functions array.
const LuauExport* PrinterModule::getExports() const {
return exports;
}
} // namespace Luwow::Printer
After implementation, our folder structure should now look like this:
Our folder structure should look like this:
printer/
├── src
│ ├── Printer
│ │ ├── PrinterModule.cpp
│ │ └── PrinterModule.h
│ └── PrinterRunner
PrinterRunner Implementation
Now that we've completed our main printer implementation, we can begin implementing our runner which will allow us to run the engine with the printer module bound. We'll do this in a new C++ file named main.cpp:
main.cpp
#include <iostream>
#include <fstream>
#include <filesystem>
#include "luacode.h"
#include "Engine.h"
#include "PrinterModule.h"
using Engine = Luwow::Engine::Engine;
using Package = Luwow::Engine::Package;
// Compiles the script from the filesystem and returns the bytecode
void compilerCallback(const std::filesystem::path& modulePath, std::string& resultingBytecode) {
std::ifstream file(modulePath);
if (!file.is_open()) {
throw std::runtime_error("Failed to open script file: " + modulePath.string());
}
std::string script(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()
);
file.close();
size_t bytecodeSize = 0;
char* bytecode = luau_compile(script.c_str(), script.length(), nullptr, &bytecodeSize);
if (!bytecode) {
throw std::runtime_error("Failed to compile script: " + modulePath.string());
}
resultingBytecode = std::string(bytecode, bytecodeSize);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: runscriptwithprinter <script.luau>" << std::endl;
return 1;
}
// Registers the Printer module to the engine.
Engine::registerNativeModule(std::make_shared<Luwow::Printer::PrinterModule>());
Engine engine((Package()), std::filesystem::path(argv[1])); // Creates the engine object with the path to the script.
engine.setCompilerCallback(compilerCallback); // Sets the compiler callback to our callback function to compile our script.
engine.initialize(argc, argv); // Initializes the engine with the arguments from the operating system.
engine.run(); // Runs the engine.
return 0;
}
We're almost done. For Luwow to compile and run this module alongside the engine, we need to define some CMakeLists.txt files.
Defining the CMakeLists.txt files
We'll create two new CMakeLists.txt files, one under Printer, and another under PrinterRunner. We'll use these files to define how the compiler should compile our module.
CMakeLists.txt under Printer
add_library(Luwow.Printer STATIC)
target_include_directories(Luwow.Printer PRIVATE
${PRINTER_ROOT}/src/Printer
${ENGINE_ROOT}
)
target_link_libraries(Luwow.Printer PRIVATE Luwow.Engine)
target_include_directories(Luwow.Printer PRIVATE ${PRINTER_ROOT}/src/Printer)
target_sources(Luwow.Printer PRIVATE
PrinterModule.h
PrinterModule.cpp
)
CMakeLists.txt under PrinterRunner
add_executable(Luwow.RunScriptWithPrinter)
target_include_directories(Luwow.RunScriptWithPrinter PRIVATE
${PRINTER_ROOT}/src/Printer
${PRINTER_ROOT}/src/PrinterRunner
${ENGINE_ROOT}
)
target_include_directories(Luwow.RunScriptWithPrinter PRIVATE ${PRINTER_ROOT}/src/Printer)
target_link_libraries(Luwow.RunScriptWithPrinter PRIVATE
Luwow.Engine
Luwow.Printer
Luau.Ast
Luau.Compiler
Luau.VM
Luau.Common
)
set_target_properties(Luwow.RunScriptWithPrinter PROPERTIES OUTPUT_NAME runscriptwithprinter)
target_sources(Luwow.RunScriptWithPrinter PRIVATE main.cpp)
As you might have noticed, we use root definitions to locate our module's root folder. This definition is made inside a main CMakeLists.txt file, apart from the ones we have defined. This is done to make the structure more modular, and gives the user control over where the folder may be defined in the filesystem.
In our case, we expect a PRINTER_ROOT to be defined inside of this main CMakeLists.txt file. You can add this definition to your main file, using the snippet below:
set(PRINTER_ROOT ${LIBRARY_ROOT}/printer)
add_subdirectory(${PRINTER_ROOT}/src/Printer)
add_subdirectory(${PRINTER_ROOT}/src/PrinterRunner)
Finally after everything above, our new folder structure should look like this:
printer/
├── src
│ ├── Printer
│ │ ├── CMakeLists.txt
│ │ ├── PrinterModule.cpp
│ │ └── PrinterModule.h
│ └── PrinterRunner
│ ├── CMakeLists.txt
│ └── main.cpp
Building and Running
After completing the above steps, we can now start the building process to create the runscriptwithprinter executable binary.
If you've been using the release repository, you can use the build commands from this page to build your project. Make sure the root definitions in the main CMakeLists.txt file are correct.
When the building process is complete, we'll have a new binary, called runscriptwithprinter. We can run this binary with a path to our script to run it. Let's create an example script like so below:
And then provide the path to this script to our binary and run it:
/path/to/runscriptwithprinter.exe /path/to/script.luau
This should print Hello from Luwow! to the output.
Conclusion
The source code of this library and the folder architecture can easily be viewed from this repository. You can clone it and update it however you wish to create your own library for Luwow.
While this tutorial showed a simple example, modules can be created for all sorts of functionality. They can even be made and built for specific platforms, which we'll cover in other tutorials.