-
-
Notifications
You must be signed in to change notification settings - Fork 4
Integration
🌐 English · 日本語
- Mana Integration
- Integration overview
- Compiler
- VM
- Program Image
- Native Functions
- SourceResolver
- Diagnostics
- Error Handling
This section explains how to embed the Mana Compiler and the Mana VM in a C++ application or a game engine.
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.
- Integration overview
- Compiler
- VM
- Program Image
- Native Functions
- SourceResolver
- Diagnostics
- Error Handling
| 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 |
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.
Mana's compiler and VM can be embedded in a C++ application as libraries.
#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.
- Turn the source into a Program Image with
mana::Compile() - If needed, check the Actors / Actions in advance with
mana::ProgramImage - Load the Program Image into
mana::VMand run it
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.
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.
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.
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.
The entry point for using the Mana Compiler from C++ is mana::Compile().
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.
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.
| 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.
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().
options.mForcedIncludeFiles.push_back("common.mn");
options.mForcedIncludeFiles.push_back("platform.mn");Files added earlier are read earlier.
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.
options.mGenerateDump = true;For analysing successes and failures, or for compiler development, a Markdown dump is available from mDump.
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.
The current compiler has global state, so Compile() cannot be called from several threads at the same time.
mana::VM is the runtime that loads a Program Image generated by the Compiler and runs its Actors / Actions.
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");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.
auto actor = vm->FindActor("Game::NPC::Guide");Actor names can be given as full names including the namespace.
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.
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().
vm->RegisterFunction("nativeAdd", &OnNativeAdd);Register a name that matches the function name declared native on the Mana side.
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.
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.
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".
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().
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"))
{
}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.
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");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.
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
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 |
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.
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 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.
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.
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);
}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.
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.
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.
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.
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.
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.
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
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.
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 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.
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.
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.
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.
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
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.
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.
mana::Diagnostic has the following information.
struct Diagnostic
{
DiagnosticSeverity mSeverity;
DiagnosticPhase mPhase;
std::string mFilename;
int32_t mLineNo;
std::string mMessage;
};mana::DiagnosticSeverity::Warning
mana::DiagnosticSeverity::Error
mana::DiagnosticSeverity::Fatalmana::DiagnosticPhase::Compile
mana::DiagnosticPhase::LinkCompile 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.
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.
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.
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
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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::ErrorWhen 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.
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.
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.
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.
| 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.
On the embedding side, it is easier to handle if you divide the responsibilities like this:
- Show Compiler problems in the editor as
Diagnostics - Treat Program Image load failures as exceptions of the loading code
- Send runtime
ScriptErrors to the Trace, and stop only the Actor in question - Notify developers of
FatalErrorthrough the Trace + FaultHandler - Set the
TraceHandlerandFaultHandlerwhen 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.
このマニュアルは shun126/Mana の documents/wiki/ から自動生成しています。Wiki を直接編集しても次の公開で上書きされるため、修正はリポジトリへの Pull Request でお願いします。
This manual is generated from documents/wiki/ in shun126/Mana. Edits made on the Wiki itself are overwritten on the next publish, so please send changes as pull requests to the repository.