Skip to content

Integration

github-actions[bot] edited this page Sep 27, 2026 · 6 revisions

C++ Integration

🌐 English · 日本語

Contents

Mana Integration

This section explains how to embed the Mana Compiler and the Mana VM in a C++ application or a game engine.

The overall picture

Mana source
   |
   v
mana::Compile()
   |
   v
CompileResult::mProgramImage
   |
   +--> Inspect its contents with mana::ProgramImage
   |
   v
mana::VM::LoadProgram()
   |
   +--> Connect to host features with Native Functions
   |
   v
VM::Run()

The way source is supplied to the Compiler can be replaced through SourceResolver. Compile-time problems are reported to the host through Diagnostic, and run-time problems through Trace / ScriptError / FatalError and so on.

Reading order

  1. Integration overview
  2. Compiler
  3. VM
  4. Program Image
  5. Native Functions
  6. SourceResolver
  7. Diagnostics
  8. Error Handling

Find by purpose

What you want to do Page
See the overall picture of embedding Mana in a game Integration overview
Compile Mana source from C++ Compiler
Run a Program Image VM
Get the list of Actors / Actions without running Program Image
Call game-side C++ from Mana Native Functions
Supply source from an editor or assets SourceResolver
Show compile errors in an IDE or CI Diagnostics
Handle runtime errors and internal Faults Error Handling

Who this is for

Whereas the Language Reference is for people writing Mana scripts, Integration is for C++ developers embedding Mana in games and tools.

To look up the Mana language itself, see the Language Reference.

Integration overview

Mana's compiler and VM can be embedded in a C++ application as libraries.

Minimal setup

#include "compiler/Compiler.h"
#include "runner/Mana.h"

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

const mana::CompileResult result = mana::Compile(options);
if (!result.mSucceeded)
    return;

auto image = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);
auto vm = std::make_shared<mana::VM>();
vm->LoadProgram(std::shared_ptr<const void>(image, image->data()));

while (vm->Run())
{
}

On the embedding side, there are three broad stages.

  1. Turn the source into a Program Image with mana::Compile()
  2. If needed, check the Actors / Actions in advance with mana::ProgramImage
  3. Load the Program Image into mana::VM and run it

Keep the Compiler and VM separate

The Mana Compiler does not assume it writes files. Compile() returns a CompileResult, and the Program Image is available as a std::vector<uint8_t>.

So it can be embedded in in-editor compilation, asset builds, server-side builds and so on.

The VM receives a compiled Program Image and runs it. You can also leave the Compiler out of the runtime and ship only pre-built Program Images.

Supplying source from somewhere other than files

By replacing CompileOptions::mSourceResolver, you can supply source from somewhere other than the file system.

For example:

  • An editor's unsaved buffer
  • A game engine's asset system
  • Virtual files inside a package
  • In-memory sources for tests

The details come later, on the SourceResolver page.

Handling errors

Compile errors are stored in CompileResult::mDiagnostics.

Compile() is designed to catch internal exceptions and not let exceptions cross over to the C++ caller. Problems while the VM runs, on the other hand, are handled as Trace output and execution state.

About threads

The current Compile() keeps global state inside the compiler, so it cannot run from several threads at the same time.

Even when building several assets in parallel, serialise the part that calls the Mana Compiler.

Related topics

Compiler

The entry point for using the Mana Compiler from C++ is mana::Compile().

Basic form

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

mana::CompileResult result = mana::Compile(options);

Compile() writes no files; it stores what it produces in a CompileResult and returns it.

CompileOptions

The main settings are:

Member What it is
mSourceFilename The entry source file
mForcedIncludeFiles Files read before the source. Equivalent to -I in the CLI
mGenerateDump Generates a dump of the symbol table, syntax tree and intermediate code
mGeneratePublicTypeDecl Generates a C++ type declaration header
mSourceResolver Replaces how source is supplied
mDiagnosticHandler Callback called when a diagnostic occurs

If mSourceResolver is left out, source is read from the standard file system.

CompileResult

Member What it is
mSucceeded true if there were no errors
mProgramImage The generated Program Image
mPublicTypeDecl The public C++ type declarations
mDump A Markdown dump for debugging
mDiagnostics All diagnostics that occurred

If compilation fails, mProgramImage is empty.

Showing diagnostics as they occur

options.mDiagnosticHandler = [](const mana::Diagnostic& diagnostic)
{
    std::cerr << diagnostic.ToString() << '\n';
};

Even with a handler, diagnostics are also kept in CompileResult::mDiagnostics.

In the current implementation, even if the diagnostic handler itself throws an exception, it is handled so that the exception does not cross out of Compile().

Forced includes

options.mForcedIncludeFiles.push_back("common.mn");
options.mForcedIncludeFiles.push_back("platform.mn");

Files added earlier are read earlier.

Generating public type declarations

options.mGeneratePublicTypeDecl = true;
const mana::CompileResult result = mana::Compile(options);

if (result.mSucceeded)
{
    const std::string& header = result.mPublicTypeDecl;
}

You get the same generation as -t in the CLI, as a string rather than a file.

Generating a dump

options.mGenerateDump = true;

For analysing successes and failures, or for compiler development, a Markdown dump is available from mDump.

Exception boundary

Compile() is designed to catch exceptions that occur inside it and turn them into fatal diagnostics. Normally the host only needs to check mSucceeded and mDiagnostics, and does not need to build control flow around Mana's internal exceptions.

Thread safety

The current compiler has global state, so Compile() cannot be called from several threads at the same time.

Related topics

VM

mana::VM is the runtime that loads a Program Image generated by the Compiler and runs its Actors / Actions.

Loading a Program Image

You can load a Program Image from memory.

auto image = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);
auto vm = std::make_shared<mana::VM>();

vm->LoadProgram(std::shared_ptr<const void>(image, image->data()));

If it has been saved as a file, you can also load it from a path.

vm->LoadProgram("game.mx");

Running

while (vm->Run())
{
}

Run() returns true while the VM's work continues.

When embedding it in a game loop, you can call Run() at the host's update rate.

Finding an Actor

auto actor = vm->FindActor("Game::NPC::Guide");

Actor names can be given as full names including the namespace.

Requesting from C++

You can send a Request to the VM as a whole, by name.

vm->Request(10, "Game::NPC::Guide", "talk", nullptr);

There is also a route that gets the Actor first and calls it directly.

Creating Actors

There is an API that copies an existing Actor, and one that creates an Actor from a Phantom.

auto clone = vm->CloneActor(actor, "GuideClone");
auto enemy = vm->CreateActorFromPhantom("EnemyTemplate", "Enemy01");

A Phantom is not created as an ordinary Actor when the Program Image is loaded; it is created explicitly with CreateActorFromPhantom().

Registering Native Functions

vm->RegisterFunction("nativeAdd", &OnNativeAdd);

Register a name that matches the function name declared native on the Mana side.

Startup when a program loads

After loading a program, the current VM Requests init at the highest Priority (2147483647) and main at Priority 0 for the ordinary Actors.

init : Priority 2147483647
main : Priority 0

So init can run its initialisation at a higher Priority than main.

Execution state

The main APIs for checking state include:

  • IsRunning()
  • GetFrameCounter()
  • GetDeltaTime()
  • IsFrameChanged()

When working with a game engine, you can treat the VM not as a standalone application but as part of the host's update loop.

Related topics

Program Image

mana::ProgramImage is a class for loading a compiled Program Image and examining the Actors / Actions / Phantoms it contains before running it.

Whereas mana::VM is the API for "running", ProgramImage is the API for "examining the contents".

Loading

auto bytes = std::make_shared<std::vector<uint8_t>>(result.mProgramImage);

mana::ProgramImage image;
const bool loaded = image.LoadProgram(
    std::shared_ptr<const void>(bytes, bytes->data()),
    bytes->size());

The result of loading is returned as a bool.

if (!loaded)
{
    std::cerr << image.GetLastError() << '\n';
}

You can also check the state with IsLoaded().

Getting the list of Actors

for (std::string_view name : image.GetActorNames())
{
    std::cout << name << '\n';
}

You can also check whether a particular Actor exists.

if (image.HasActor("Game::NPC::Guide"))
{
}

Examining an Actor's Actions

for (std::string_view action :
     image.GetActorActionNames("Game::NPC::Guide"))
{
    std::cout << action << '\n';
}
if (image.HasActorAction("Game::NPC::Guide", "talk"))
{
}

Use it to validate names before the game sends a Request by name, or to build lists of candidates in an editor UI.

Examining Phantoms

For Phantoms too, you can check existence and get the list of Actions.

if (image.HasPhantom("EnemyTemplate"))
{
    const auto actions =
        image.GetPhantomActionNames("EnemyTemplate");
}
image.HasPhantomAction("EnemyTemplate", "damage");

Lifetime of the Program Image

ProgramImage holds the loaded bytes as a shared_ptr<const void>.

Actor and Action names are returned as std::string_view, so these views depend on the lifetime of the data the Program Image holds. Don't keep a string_view you got for longer than the ProgramImage it came from.

Uses

ProgramImage is especially suited to:

  • Showing the list of Actors in an editor
  • Choosing an Action name from a combo box
  • Validating in advance the Actor / Action names given in C++-side settings
  • Listing candidates for creating Phantoms
  • Inspecting a Program Image without running it

How it differs from the VM

ProgramImage VM
Reads a Program Image Yes Yes
Examines lists of Actors / Actions Its main use Has some lookup APIs
Runs Actions No Yes
Sends Requests No Yes
Registers Native Functions No Yes

Related topics

Native Functions

Native Functions are the boundary for calling a game's or tool's C++ code from a Mana script.

On the Mana side you declare a function with native, and on the C++ side you register a function with the same name with mana::VM.

Minimal setup

On the Mana side, declare it like this.

native int add(int a, int b);

actor Main
{
    action main()
    {
        int result = add(10, 20);
        print("%d\n", result);
    }
}

On the C++ side, register the external function.

void Add(const std::shared_ptr<mana::Actor>& actor, void*)
{
    const int32_t a = actor->GetParameterInteger(0);
    const int32_t b = actor->GetParameterInteger(1);
    actor->SetReturnInteger(a + b);
}

std::shared_ptr<mana::VM> vm = std::make_shared<mana::VM>();
vm->RegisterFunction("add", &Add);

Load a Program Image into the VM you registered with and Run() as usual, and a call to add() in Mana runs the C++ Add().

The external function type

The current VM's external function type has this form:

using ExternalFunctionType =
    std::function<void(const std::shared_ptr<mana::Actor>& actor,
                       void* structPointer)>;

The first argument, actor, is the Actor running that native function.

The second argument, structPointer, is used to refer to the Struct instance when a Struct's native method was called.

Getting the arguments

A Native Function's arguments are taken from the running Actor.

The typical APIs are:

actor->GetParameterInteger(index);
actor->GetParameterFloat(index);
actor->GetParameterString(index);
actor->GetParameterActor(index);
actor->GetParameterPointer(index);
actor->GetParameterAddress(index);

The number of arguments is available through:

const int32_t count = actor->GetArgumentCount();

Keep the types and order read on the C++ side matching the declaration on the Mana side, as a contract of the embedding.

Returning a value

For a native function with a return value, use SetReturn*().

actor->SetReturnInteger(value);
actor->SetReturnFloat(value);
actor->SetReturnString(text);
actor->SetReturnActor(otherActor);
actor->SetReturnPointer(pointer);
actor->SetReturnData(data, size);

For example, if the Mana side is

native float getSpeed();

then the C++ side sets the value like this.

void GetSpeed(const std::shared_ptr<mana::Actor>& actor, void*)
{
    actor->SetReturnFloat(3.5f);
}

Native methods on Structs

In Mana you can declare a native function as a member of a Struct.

struct Position
{
    float x;
    float y;

    native void normalize();
}

In this case, the external name resolved on the C++ side is StructName::methodName.

vm->RegisterFunction("Position::normalize", &NormalizePosition);

The callback's second argument, structPointer, receives the address of the target Struct.

void NormalizePosition(const std::shared_ptr<mana::Actor>&,
                       void* structPointer)
{
    // Treat structPointer as the matching layout on the host side
}

If you handle a Struct's memory layout directly on the C++ side, keep it strictly in step with the type definition on the Mana side.

Registering C++ member functions

With VM::RegisterMemberFunction(), you can register a C++ object's member function without a wrapper.

class GameBridge
{
public:
    void PlaySound(const std::shared_ptr<mana::Actor>& actor, void* structPointer)
    {
        // Game-side work
    }
};

auto bridge = std::make_shared<GameBridge>();
vm->RegisterMemberFunction("playSound", bridge, &GameBridge::PlaySound);

The current API has overloads that take a raw pointer, a shared_ptr and a weak_ptr.

The overload that takes a shared_ptr keeps it internally as a weak reference. If the registered object is destroyed first, an error is written to the Trace when it is called.

Make the registered names match

At run time the VM looks up external functions by their string names.

If the name declared on the Mana side and the name registered with RegisterFunction() don't match, the VM reports in the Error Trace that the external function cannot be found.

When embedding, we recommend registering every Native Function you need before loading and running the Program Image.

Its role in the design

Native Functions are not a way of bringing rendering, physics, sound, asset management and so on into Mana itself.

Designing them as a boundary that leaves those in the host application and exposes to Mana only the operations it needs makes it easy to keep the responsibilities of scripts and the game engine apart.

Related topics

SourceResolver

mana::SourceResolver is the interface that supplies source code to the Mana Compiler.

Normally .mn files are read from the file system, but you can also supply source from an editor's unsaved buffer, an asset database, data inside a package, virtual files over a network, and so on.

How it relates to the Compiler

Set your own Resolver in CompileOptions::mSourceResolver.

mana::CompileOptions options;
options.mSourceFilename = "main.mn";
options.mSourceResolver = resolver;

mana::CompileResult result = mana::Compile(options);

If mSourceResolver is left out, the default mana::FileSourceResolver is used.

The interface

A custom Resolver implements these two functions.

class SourceResolver
{
public:
    virtual std::string Resolve(
        std::string_view from,
        std::string_view filename) const = 0;

    virtual bool Read(
        std::string_view path,
        std::string& outText) const = 0;
};

Their roles are clearly separate.

  • Resolve() : decides, from the referring source and the given name, the location that identifies the source
  • Read() : gets the actual source text from the resolved location

FileSourceResolver

The standard FileSourceResolver reads from the file system.

project/
├─ main.mn
└─ actor/
   └─ npc.mn

If main.mn contains

import "actor/npc.mn";

the relative path is resolved from the directory that contains main.mn.

If actor/npc.mn then reads another file, the location of npc.mn becomes the base.

Only the first source is resolved from the current working directory.

Supplying source from memory

For example, to compile an editor's unsaved contents directly, you can write a Resolver like this.

class MemorySourceResolver final : public mana::SourceResolver
{
public:
    std::map<std::string, std::string, std::less<>> files;

    std::string Resolve(
        std::string_view,
        std::string_view filename) const override
    {
        return std::string(filename);
    }

    bool Read(
        std::string_view path,
        std::string& outText) const override
    {
        const auto it = files.find(path);
        if (it == files.end())
            return false;

        outText = it->second;
        return true;
    }
};

The calling side looks like this.

auto resolver = std::make_shared<MemorySourceResolver>();
resolver->files["main.mn"] = R"(
actor Main
{
    action main()
    {
        print("Hello\n");
    }
}
)";

mana::CompileOptions options;
options.mSourceFilename = "main.mn";
options.mSourceResolver = resolver;

const mana::CompileResult result = mana::Compile(options);

import and unique paths

import does not read a source with the same resolved path twice.

So in a custom SourceResolver, it is important to return, as far as possible, the same resolved string for names that refer to the same source.

For example, if these two refer to the same data

scripts/npc.mn
scripts/./npc.mn

but you return them as different resolved results, the Compiler may see them as different sources.

We recommend normalising to a stable identifier, such as an asset ID or a normalised virtual path.

Read failures

If Resolve() returns an empty string, the Compiler diagnoses that the location given could not be resolved.

If Resolve() returns a location but Read() returns false, it diagnoses that the resolved location could not be opened.

In editor integrations, returning from Resolve() a logical path that reads well in diagnostics makes it easy to show users which virtual file had the problem.

Line endings

The Resolver does not need to unify the line endings of the string Read() returns.

The Compiler's Lexer normalises them to LF after reading.

Forced reading, equivalent to -I

Sources given in CompileOptions::mForcedIncludeFiles are also read through the same SourceResolver.

options.mForcedIncludeFiles.push_back("common.mn");

This is equivalent to -I common.mn in the CLI.

Example uses

Replacing the SourceResolver enables integrations such as:

  • Compiling an unsaved Mana script in a game editor as it is
  • Supplying source from assets in Unreal Engine and similar engines
  • Reading from inside packages such as zip / pak
  • Giving source to unit tests without file I/O
  • Separating logical paths from where files actually are

Related topics

Diagnostics

The Mana Compiler does not just send warnings and errors to standard output as plain strings; it returns them in structured form as mana::Diagnostic.

When embedding it in an editor, IDE, CI or in-game tool, CompileResult::mDiagnostics lets you show them your own way while keeping the file name and line number.

Getting them from CompileResult

The most basic way is to check mDiagnostics after compiling.

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

const mana::CompileResult result = mana::Compile(options);

for (const mana::Diagnostic& diagnostic : result.mDiagnostics)
{
    std::cout << diagnostic.ToString() << '\n';
}

Having diagnostics does not always mean compilation failed.

  • Warning: a warning. Compilation continues
  • Error: an error. Analysis continues as far as it can, but nothing is produced
  • Fatal: a fatal error that stops that compilation from continuing

Use CompileResult::mSucceeded as the final judgement of success.

What a Diagnostic contains

mana::Diagnostic has the following information.

struct Diagnostic
{
    DiagnosticSeverity mSeverity;
    DiagnosticPhase mPhase;
    std::string mFilename;
    int32_t mLineNo;
    std::string mMessage;
};

Severity

mana::DiagnosticSeverity::Warning
mana::DiagnosticSeverity::Error
mana::DiagnosticSeverity::Fatal

Phase

mana::DiagnosticPhase::Compile
mana::DiagnosticPhase::Link

Compile covers problems in lexical analysis, parsing, semantic analysis, code generation and so on.

Link covers problems in symbol resolution and in producing the Program Image.

File name and line number

With mFilename and mLineNo, an editor can jump to the source in question.

for (const auto& diagnostic : result.mDiagnostics)
{
    editor.ShowDiagnostic(
        diagnostic.mFilename,
        diagnostic.mLineNo,
        diagnostic.mMessage);
}

mLineNo == 0 means the diagnostic has no line information.

A diagnostic that occurs in a source brought in with include / import keeps that source's file name and line number.

When you use a custom SourceResolver, the logical path Resolve() returns becomes important as-is, as the location shown in diagnostics.

Converting to the standard format

Diagnostic::ToString() formats it in Mana's standard format.

const std::string text = diagnostic.ToString();

With a line number, depending on the platform, it looks like this:

main.mn(12): error: message

or like this:

main.mn:12 error: message

You can use the structured fields in your own UI, and ToString() in the CLI and logs.

Receiving them as they occur

To receive diagnostics when they occur, rather than after compilation completes, use CompileOptions::mDiagnosticHandler.

mana::CompileOptions options;
options.mSourceFilename = "main.mn";

options.mDiagnosticHandler = [](const mana::Diagnostic& diagnostic)
{
    LogDiagnostic(diagnostic);
};

const mana::CompileResult result = mana::Compile(options);

Even with a Handler set, diagnostics also remain in CompileResult::mDiagnostics.

So you can use them like this:

  • Handler: real-time display and logging
  • mDiagnostics: listing after compilation, and tests

Don't throw from the Handler

We recommend designing the embedding's Diagnostic Handler so that it does not throw exceptions.

The current Compile() is implemented so that exceptions do not leave the compile boundary, and a regression test checks that an exception thrown inside the Handler does not cross over to the host either.

But if the diagnostic handling itself fails, you can lose the error display you actually wanted, so keep the Handler as simple as possible.

The Compiler's threading constraint

The current Mana Compiler uses global state, including for collecting diagnostics.

So mana::Compile() cannot be called from several threads at the same time.

Even when an editor compiles in the background, serialise the calls to the Mana Compiler into a single line.

Using them in CI

In CI it is convenient to handle mSucceeded and mDiagnostics together.

const mana::CompileResult result = mana::Compile(options);

if (!result.mSucceeded)
{
    for (const auto& diagnostic : result.mDiagnostics)
        std::cerr << diagnostic.ToString() << '\n';

    return 1;
}

If your own policy treats warnings as errors, the host can decide that by looking at DiagnosticSeverity.

Compile errors and runtime errors are separate

The Diagnostic this page covers is mainly the Compiler's diagnostics.

Script execution errors and internal VM errors that occur after a Program Image is loaded are handled through other routes, such as Trace, ScriptError and FatalError.

For those, see Error Handling.

Related topics

Error Handling

When embedding Mana in a game or tool, errors are easier to handle if you don't treat them as one kind, but think of them separately as compile time / loading the Program Image / script execution / internal inconsistencies in Mana.

The overall picture

Mana source
   |
   | Compile diagnostics
   v
mana::Compile()
   |
   | Program Image load error
   v
mana::VM::LoadProgram()
   |
   | ScriptError / runtime trace
   v
mana::VM::Run()
   |
   | FatalError / FaultHandler
   v
An inconsistency in Mana or in the embedding

Each has a different unit of recovery and a different notification route.

Compile-time errors

mana::Compile() returns failures as a CompileResult.

const mana::CompileResult result = mana::Compile(options);

if (!result.mSucceeded)
{
    for (const auto& diagnostic : result.mDiagnostics)
        ShowError(diagnostic);
}

Exceptions that occur inside the Compiler are also caught at the boundary of Compile() and stored in mDiagnostics as fatal diagnostics.

So ordinary embedding code does not need to control the whole of Compile() with exceptions because of compile errors.

For details, see Diagnostics.

Errors loading a Program Image

Unlike the Compiler API, VM::LoadProgram() may throw an exception when loading fails.

For example, the current implementation checks for problems such as:

  • The file cannot be opened
  • It does not have the signature of a Mana Program Image
  • The Program Image's version does not match
  • The 32-bit / 64-bit format does not match the runtime

On the host side, catch exceptions at the loading boundary.

try
{
    vm->LoadProgram("event.mx");
}
catch (const std::exception& e)
{
    LogError(e.what());
    return false;
}

When loading from memory too, design the host to handle exceptions for an invalid Program Image.

Checking in advance with ProgramImage

To examine a Program Image without running it, you can use mana::ProgramImage.

mana::ProgramImage image;

if (!image.LoadProgram(program, size))
{
    LogError(image.GetLastError());
    return false;
}

ProgramImage::LoadProgram() returns success as a bool, and the reason for a failure is available from GetLastError().

In an editor or asset importer, you can use it as a check before handing the image to the VM.

Script execution errors

Problems during execution of a Mana script that it cannot continue from, such as division by zero, an array index out of range, or an invalid wait on itself, are handled as mana::ScriptError.

VM::RunActor() catches ScriptError per Actor.

Actor A
  ScriptError
     |
     v
  Actor A is stopped

Actor B / C
  Keep running

The Actor that caused the error stops, but the design is not to stop the whole VM or other Actors for that reason alone.

The error details are written to the Trace as TraceLevel::Error.

Connecting the Trace to the host

Output from print(), warnings, runtime errors and so on can be received through the Trace.

void OnTrace(
    void* userData,
    const mana::TraceLevel level,
    const char* message,
    const std::size_t length)
{
    // Send it to the game engine's log
}

mana::SetTraceHandler(&OnTrace, hostContext);

There are three severities.

mana::TraceLevel::Info
mana::TraceLevel::Warning
mana::TraceLevel::Error

When embedding in a game engine where standard output is hard to use, we recommend setting a TraceHandler at startup and connecting it to the engine's log.

Notes on the TraceHandler

Set the TraceHandler once before you start using it, and don't swap it frequently while running.

Also, don't throw exceptions from the Handler itself.

The Trace is not always called back one line at a time, so if you need line-by-line logs, buffer on the host side until a line break.

Internal inconsistencies in Mana

When an internal assumption breaks, not because of a script mistake but because of an inconsistency in Mana itself or in the embedding, mana::FatalError is used.

This family is reported from RaiseFault(), which writes an Error Trace and then throws FatalError.

A std::exception that occurs while an Actor runs is caught at the boundary of VM::RunActor(), and that Actor stops. Other Actors can continue.

FaultHandler

To connect internal inconsistencies to a debugger or crash reporting, you can use SetFaultHandler().

void OnFault(
    void* userData,
    const char* file,
    int line,
    const char* message)
{
    // debugger break / crash reporter / telemetry and so on
}

mana::SetFaultHandler(&OnFault, hostContext);

The FaultHandler is called before unwinding starts, so you can also use it to stop in the debugger on the spot while developing Mana.

After the Handler returns, FatalError is thrown.

The FaultHandler itself must not throw exceptions.

ScriptError and FatalError

Kind Meaning FaultHandler Actor
ScriptError A runtime error on the script side Not called The Actor in question is stopped
FatalError An internal inconsistency in Mana or the embedding Called At an Actor execution boundary, the Actor in question is stopped

It is important to keep errors shown to script authors apart from internal inconsistencies that Mana / engine developers should investigate.

Recommended approach on the host side

On the embedding side, it is easier to handle if you divide the responsibilities like this:

  1. Show Compiler problems in the editor as Diagnostics
  2. Treat Program Image load failures as exceptions of the loading code
  3. Send runtime ScriptErrors to the Trace, and stop only the Actor in question
  4. Notify developers of FatalError through the Trace + FaultHandler
  5. Set the TraceHandler and FaultHandler when the application initialises

This lets you choose a unit of recovery that fits the kind of problem, rather than ending the whole game process immediately because of one invalid script.

Related topics

Clone this wiki locally