Explore documentation, tutorials, and practical guidance for ni ecosystem.
RAII ties a resource to an object's lifetime: acquire it in the constructor, release it in the destructor. Whatever happens next (early return, exception), the cleanup runs.
std::unique_ptr is RAII for heap memory with exactly one owner. It can't be copied, only moved, so the code itself tells you who owns what.
#include <memory>
#include <cstdio>
struct Runtime {
Runtime() { std::puts("start"); }
~Runtime() { std::puts("stop"); }
};
int main() {
auto rt = std::make_unique<Runtime>(); // rt owns it
auto other = std::move(rt); // ownership moves, rt is now null
} // "stop" prints once, when `other` leaves scopeP/Invoke lets C# call functions exported from a native DLL. The call itself is easy; the risk is that both sides must agree on the exact memory layout of every struct.
Use StructLayout(Sequential), match field types and sizes, and be careful with bool (marshalled as 4 bytes by default) and strings (pick a CharSet explicitly). On the native side, export with extern "C" to avoid name mangling.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct Point { public int X; public int Y; }
static class Native
{
[DllImport("native.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetDistance(ref Point a, ref Point b);
}
// native side (C++):
// struct Point { int x; int y; };
// extern "C" __declspec(dllexport) int GetDistance(Point* a, Point* b);When the type is known at compile time, a template avoids virtual calls and doesn't force unrelated types to share a base class.
Before C++20 you constrained templates with SFINAE (std::enable_if), which produces unreadable errors. A concept states the same requirement directly, and the compiler error names it.
#include <concepts>
template <typename T>
concept Drawable = requires(T t) {
{ t.draw() } -> std::same_as<void>;
};
template <Drawable T>
void render(T& item) {
item.draw(); // resolved at compile time, no vtable
}Embedding Lua gives users a way to extend the app without recompiling it. The binding library (NLua is one option) is the small part.
The real design work is deciding what scripts may touch. Expose one small API object instead of your forms or internals, and keep it stable, since scripts will depend on it.
public class ScriptApi
{
public string Version => "1.0";
public void Log(string msg) => Console.WriteLine(msg);
}
using var lua = new NLua.Lua();
lua["app"] = new ScriptApi(); // the only thing scripts can see
lua.DoString("app:Log('loaded ' .. app.Version)");Think in targets, not global variables. Attach include paths, definitions, and libraries to a target with target_* commands, and dependencies propagate correctly.
Keep platform branches small and close to the target that needs them. The same file then builds on macOS and Linux.
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app src/main.cpp)
target_include_directories(app PRIVATE include)
if(APPLE)
target_compile_definitions(app PRIVATE PLATFORM_MACOS)
elseif(UNIX)
target_compile_definitions(app PRIVATE PLATFORM_LINUX)
endif()In a kernel there is no OS underneath you. Compile with -ffreestanding -fno-exceptions -fno-rtti and you lose the standard library, new/delete, exceptions, and RTTI.
You keep the language itself: templates, constexpr, classes, and the freestanding headers such as <cstdint> and <type_traits>. Anything else, including memcpy, you provide yourself.
// g++ -ffreestanding -fno-exceptions -fno-rtti -nostdlib -c kernel.cpp
#include <cstdint>
extern "C" void kernel_main() {
auto vga = reinterpret_cast<volatile std::uint16_t*>(0xB8000); // x86 text mode
const char* msg = "hello";
for (int i = 0; msg[i]; ++i)
vga[i] = static_cast<std::uint16_t>(0x0F00 | msg[i]); // white on black
for (;;) {}
}A where clause is checked by the compiler and shows up in tooling, so it stays true in a way a comment doesn't.
You can combine constraints: an interface, a base class, and new() (requires a public parameterless constructor) in one clause.
public interface IComponent { void Update(float dt); }
public class Registry
{
readonly List<IComponent> _items = new();
public T Add<T>() where T : IComponent, new()
{
var c = new T();
_items.Add(c);
return c;
}
}std::move doesn't move anything. It is a cast to an rvalue reference; the actual transfer happens in the move constructor or move assignment that gets picked.
Don't write return std::move(local). It can block copy elision, and returning a local already moves when needed. After a move, the source is valid but unspecified: assign to it or destroy it, nothing else.
std::vector<int> make() {
std::vector<int> v(1'000'000);
return v; // elided or moved automatically
}
std::vector<int> a = make();
std::vector<int> b = std::move(a); // b takes a's buffer, no element copies