utilities.md 26 KB

Utilities {#utilities}

[TOC]

This manual will quickly go over all the important utility systems commonly used by Banshee. We won't go into major detail about these features, but will rather point you towards the relevant API documentation.

Module {#utilities_a}

A @ref BansheeEngine::Module "Module" is a specialized form of singleton used for many of Banshee's systems. Unlike normal singletons it requires manual startup and shutdown, which solves many of the problems associated with traditional singletons.

To use it for your own objects, simply inherit from it:

class MyModule : public Module<MyModule>
{ };

Use @ref BansheeEngine::Module::startUp "Module::startUp" to start it up. Once started use @ref BansheeEngine::Module::instance "Module::instance" to access its instance. Once done with it call @ref BansheeEngine::Module::shutDown "Module::shutDown" to release it. For example:

MyModule::startUp();
MyModule::instance().doSomething();
MyModule::shutDown();

Path {#utilities_b}

Use @ref BansheeEngine::Path "Path" to manipulate file/folder paths. Initialize it with a path string and then call various methods to manipulate it. It is recommended to always store paths using @ref BansheeEngine::Path "Path" instead of raw strings.

Some of the things you can do once a @ref BansheeEngine::Path "Path" is constructed:

  • Retrieve the filename using @ref BansheeEngine::Path::getFilename "Path::getFilename"
  • Retrieve the filename extension using @ref BansheeEngine::Path::getExtension "Path::getExtension"
  • Get last element of path, either file or directory using @ref BansheeEngine::Path::getTail "Path::getTail"
  • Iterate over directories, get drive, combine paths, convert relative to absolute paths and vice versa, and more...

For example:

Path myPath("C:\\Path\\To\\File.txt");

String filename = myPath.getFilename(); // Returns file
myPath.setExtension(".jpg"); // Path is now "C:\Path\To\File.jpg"
myPath.makeRelative("C:\\Path"); // Path is now "To\File.jpg"

Path a("C:\\Path\\To\\");
Path b("File.txt");
Path combined = a + b; // // Path is now "C:\Path\To\File.txt"

All @ref BansheeEngine::Path "Path" methods that return strings come in two variants, one that returns a narrow (8-bit) character string like @ref BansheeEngine::Path::getFilename "Path::getFilename", and one that contains wide character string like @ref BansheeEngine::Path::getWFilename "Path::getWFilename".

When setting paths be careful with setting backslashes or slashes at the end of the path. Path with a no backslash/slash on the end will be interpreted as a file path, and path with a backslash/slash will be interpreted as a folder path. For example:

  • "C:\MyFolder" - "MyFolder" interpreted as a file, @ref BansheeEngine::Path::getFilename "Path::getFilename" returns "MyFolder"
  • "C:\MyFolder\" - "MyFolder" interpreted as a folder, @ref BansheeEngine::Path::getFilename "Path::getFilename" returns an empty string

File system {#utilities_c}

The @ref BansheeEngine::FileSystem "FileSystem" module allows you to open and create files, create folders, move/copy/remove files and folders, iterate all folder/files in a folder, get file size, check if folder/folder exists, get working path and others.

An example creating a folder and a file:

FileSystem::createDir("C:\\Path\\To\\");
SPtr<DataStream> fileStream = FileSystem::createAndOpenFile("C:\\Path\\To\\File.txt");
... write to data stream...

Data streams {#utilities_d}

@ref BansheeEngine::DataStream "Data streams" allow you to easily write/read binary/text data from/to disk/memory/etc. The two primary types of streams are @ref BansheeEngine::MemoryDataStream "MemoryDataStream" for reading/writing directly to memory, and @ref BansheeEngine::FileDataStream "FileDataStream" for reading/writing to a file.

You create memory streams by providing them with a pointer and size of a memory buffer, while you create file streams by calling @ref BansheeEngine::FileSystem::openFile "FileSystem::openFile" or @ref BansheeEngine::FileSystem::createAndOpenFile "FileSystem::createAndOpenFile". Once you are done with a stream make sure to close it by calling @ref BansheeEngine::DataStream::close "DataStream::close". Stream will also be automatically closed when it goes out of scope.

Once you have a stream you can seek to a position within a stream and read/write to it. For example:

SPtr<DataStream> fileStream = FileSystem::createAndOpenFile("C:\\Path\\To\\File.txt");
// Write some string data
fileStream.writeString("Writing to a file");
// Write some binary data
UINT8* myBuffer = bs_alloc(1024);
... fill up the buffer with some data ...
fileStream.write(myBuffer, 1024);

fileStream.close();

Events {#utilities_e}

@ref BansheeEngine::TEvent "Events" allow your objects to expose events that may trigger during execution. External objects interested in those events can then register callbacks with those events and be notified when they happen. They are useful because they allow two objects to communicate without necessarily knowing about each other's types, which can reduce class coupling and improve design.

When creating an event, all you need to do it specify a format of the callback it sends out, for example:

class MySystem
{
	static Event<void()> myEvent; // Callback with no parameters
	static Event<void(UINT32)> myEvent2; // Callback with a UINT32 parameter
};

Then an external object can register itself with an event by calling @ref BansheeEngine::TEvent "Event::connect". This method will return an @ref BansheeEngine::HEvent "HEvent" handle. You can use this handle to manually disconnect from the event by calling @ref BansheeEngine::HEvent::disconnect "HEvent::disconnect". For example:

// Subscribe to an event we defined previously
// Simply pass a function pointer matching the callback
HEvent eventHandle = MySystem::myEvent2.connect(&myEventReceiver);

void myEventReceiver(UINT32 val)
{
	// Do something
}

When using non-static class methods as callbacks, things get a little bit more complicated. This is because each such method by default expects a pointer to an instance of itself (this pointer). This is normally hidden from the programmer and happens under the hood, but we must handle it when dealing with callbacks. We can do this by using std::bind which allows us to replace function arguments with constant values. For example:

class EventReceiver
{
	EventReceiver()
	{
		// Convert a method with signature void(EventReceiver*, UINT32) into void(UINT32) by binding the "EventReceiver*"
		// argument to the value of "this". Read up on the C++ library for more information about std::bind.
		MySystem::myEvent2.connect(std::bind(&EventReceiver::myEventReceiver, this, std::placeholders::_1));
	}
	
	void myEventReceiver(UINT32 val)
	{
		// Do something
	}
};

Then when an object is ready to trigger an event simply call it like a functor:

MySystem::myEvent(); // Trigger an event with no arguments
MySystem::myEvent2(5); // Trigger an event with a single argument

Any {#utilities_f}

Use the @ref BansheeEngine::Any "Any" type to easily store any kind of object in it. For example:

Any var1 = Vector<String>();

struct MyStruct { int a; };
Any var2 = MyStruct();

Use @ref BansheeEngine::any_cast "any_cast" and @ref BansheeEngine::any_cast_ref "any_cast_ref" to retrieve valid types from an @ref BansheeEngine::Any "Any" variable. For example:

Vector<String> val1 = any_cast<Vector<String>>(var1);
MyStruct& val2 = any_cast_ref<MyStruct>(var2);

Flags {#utilities_g}

@ref BansheeEngine::Flags "Flags" provide a wrapper around an enum and allow you to easily perform bitwise operations on them without having to cast to integers. For example when using raw C++ you must do something like this:

enum class MyFlag
{
	Flag1 = 1<<0,
	Flag2 = 1<<1,
	Flag3 = 1<<2
};

MyFlag combined = (MyFlag)((UINT32)MyFlag::Flag1 | (UINT32)MyFlag::Flag2);

Which is cumbersome. Flags require an additional step to define the enum, but after that allow you to manipulate values much more nicely.

To create @ref BansheeEngine::Flags "Flags" for an enum simply define a typedef with your enum type provided as the template parameter. You must also follow that definition with a @ref BS_FLAGS_OPERATORS macro in order to ensure all operators are properly defined. For example:

typedef Flags<MyFlag> MyFlags;
BS_FLAGS_OPERATORS(MyFlag)

Now you can do something like this:

MyFlags combined = MyFlag::Flag1 | MyFlag::Flag2;

String {#utilities_h}

Banshee uses @ref BansheeEngine::String "String" for narrow character strings (8-bit) and @ref BansheeEngine::WString "WString" for wide character strings. Wide character strings are different size depending on platform.

A variety of string manipulation functionality is provided in @ref BansheeEngine::StringUtil "StringUtil", like matching, replacing, comparing, formating and similar.

Conversion between various types (like int, float, bool, etc.) and string is provided via overloads of @ref BansheeEngine::toString "toString" and @ref BansheeEngine::toWString "toWString". You can also convert strings into different types by calling methods like @ref BansheeEngine::parseINT32 "parseINT32", @ref BansheeEngine::parseBool "parseBool", and similar for other types.

Threading {#utilities_i}

Primitives {#utilities_i_a}

This section describes the most basic primitives you can use to manipulate threads. All threading primitives use the standard C++ library constructs, so for more information you should read their documentation.

Thread {#utilities_i_a_a}

To create a new thread use @ref BansheeEngine::Thread "Thread", like so:

void workerFunc()
{
	// This runs on another thread
}

Thread myThread(&workerFunc);

Mutex {#utilities_i_a_b}

Use @ref BansheeEngine::Mutex "Mutex" and @ref BansheeEngine::Lock "Lock" to synchronize access between multiple threads, like so:

Vector<int> output;
int startIdx = 0;
Mutex mutex;

void workerFunc()
{
	// Lock the mutex before modifying either "output" or "startIdx"
	// This ensures only one thread every accesses it at once
	Lock lock(mutex);
	output.push_back(startIdx++);
}

// Start two threads that write to "output"
Thread threadA(&workerFunc);
Thread threadB(&workerFunc);

If a mutex can be locked recursively, use @ref BansheeEngine::RecursiveMutex "RecursiveMutex" and @ref BansheeEngine::RecursiveLock "RecursiveLock" instead.

Signal {#utilities_i_a_c}

Use @ref BansheeEngine::Signal "Signal" to pause thread execution until another thread reaches a certain point. For example:

bool isReady = false;
int result = 0;

Signal signal;
Mutex mutex;

void workerFunc()
{
	for(int i = 0; i < 100000; i++)
		result += i; // Or some more complex calculation
	
	// Lock the mutex so we can safely modify isReady
	{
		Lock lock(mutex);
		isReady = true;		
	} // Automatically unlocked when lock goes out of scope
	
	// Notify everyone waiting that the signal is ready
	signal.notify_all();
}

// Start executing workerFunc
Thread myThread(&workerFunc);

// Wait until the signal is triggered, or until isReady is set to true, whichever comes first
Lock lock(mutex);
if(!isReady)
	signal.wait_for(lock);

Other {#utilities_i_a_d}

The previous sections covered all the primitives, but there is some more useful functionality to be aware of:

  • @ref BS_THREAD_HARDWARE_CONCURRENCY - Returns number of logical CPU cores.
  • @ref BS_THREAD_CURRENT_ID - Returns @ref BansheeEngine::ThreadId "ThreadId" of the current thread.
  • @ref BS_THREAD_SLEEP - Pauses the current thread for a set number of milliseconds.

Thread pool {#utilities_i_b}

Instead of using @ref BansheeEngine::Thread "Thread" as described in the previous section, you should instead use @ref BansheeEngine::ThreadPool "ThreadPool" for running threads. @ref BansheeEngine::ThreadPool "ThreadPool" allows you to re-use threads and avoid paying the cost of thread creation and destruction. It keeps any thread that was retired in idle state, and will re-use it when user requests a new thread.

An example:

void workerFunc()
{
	// This runs on another thread
}

ThreadPool::instance().run("MyThread", &workerFunc);

Task scheduler {#utilities_i_c}

@ref BansheeEngine::TaskScheduler "TaskScheduler" allows even more fine grained control over threads. It ensures there are only as many threads as the number of logical CPU cores. This ensures good thread distribution accross the cores, so that multiple threads don't fight for resources on the same core.

It accomplishes that by storing each worker function as a @ref BansheeEngine::Task "Task". It then dispatches tasks to threads that are free. In case tasks are dependant on one another you may also provide task dependencies, as well as task priorities.

An example:

void workerFunc()
{
	// This runs on another thread
}

// Create a task with no dependency and normal priority
SPtr<Task> task = Task::create("MyTask", &workerFunc);
TaskScheduler::instance().addTask(task);

Math {#utilities_j}

Majority of the math related functionality is located in the @ref BansheeEngine::Math "Math" class.

Some other useful math classes are:

  • @ref BansheeEngine::Vector2 "Vector2"
  • @ref BansheeEngine::Vector3 "Vector3"
  • @ref BansheeEngine::Vector4 "Vector4"
  • @ref BansheeEngine::Matrix3 "Matrix3"
  • @ref BansheeEngine::Matrix4 "Matrix4"
  • @ref BansheeEngine::Quaternion "Quaternion"
  • @ref BansheeEngine::Radian "Radian"
  • @ref BansheeEngine::Degree "Degree"
  • @ref BansheeEngine::Ray "Ray"
  • @ref BansheeEngine::Plane "Plane"
  • @ref BansheeEngine::Rect2 "Rect2"
  • @ref BansheeEngine::Rect2I "Rect2I"
  • @ref BansheeEngine::Vector2I "Vector2I"

Time {#utilities_k}

To access timing information use the @ref BansheeEngine::Time "Time" module, more easily accessible via @ref BansheeEngine::gTime "gTime" method:

  • @ref BansheeEngine::Time::getTime "Time::getTime" - Returns time since start-up in seconds, updated once per frame.
  • @ref BansheeEngine::Time::getFrameDelta "Time::getFrameDelta" - Returns the time between execution of this and last frame.
  • @ref BansheeEngine::Time::getFrameIdx "Time::getFrameIdx" - Returns a sequential index of the current frame.
  • @ref BansheeEngine::Time::getTimePrecise "Time::getTimePrecise" - Returns time suitable for precision measurements. Returns time at the exact time it was called, instead of being updated once per frame.

Logging {#utilities_l}

To report warnings and errors use the @ref BansheeEngine::Debug "Debug" module. Call @ref BansheeEngine::Debug::logDebug "Debug::logDebug", @ref BansheeEngine::Debug::logWarning "Debug::logWarning" and @ref BansheeEngine::Debug::logError "Debug::logError" to log messages.

Use @ref BansheeEngine::Debug::saveLog "Debug::saveLog" to save a log to the disk in HTML format. Use use @ref BansheeEngine::Debug::getLog "Debug::getLog" to get a @ref BansheeEngine::Log "Log" object you can manually parse.

Macros for common log operations are also provided: @ref LOGDBG, @ref LOGWRN and @ref LOGERR. They're equivalent to the methods above.

Crash handling {#utilities_m}

Use the @ref BansheeEngine::CrashHandler "CrashHandler" to report fatal errors. Call @ref BansheeEngine::CrashHandler::reportCrash "CrashHandler::reportCrash" to manually trigger such an error. An error will be logged, a message box with relevant information displayed and the application terminated.

You can also use @ref BS_EXCEPT macro, which internally calls @ref BansheeEngine::CrashHandler::reportCrash "CrashHandler::reportCrash" but automatically adds file/line information.

@ref BansheeEngine::CrashHandler "CrashHandler" also provides @ref BansheeEngine::CrashHandler::getStackTrace "CrashHandler::getStackTrace" that allows you to retrieve a stack trace to the current method.

Dynamic libraries {#utilities_n}

Use @ref BansheeEngine::DynLibManager "DynLibManager" to load dynamic libraries (.dll, .so). It has two main methods:

  • @ref BansheeEngine::DynLibManager::load "DynLibManager::load" - Accepts a file name to the library, and returns the @ref BansheeEngine::DynLib "DynLib" object if the load is successful or null otherwise.
  • @ref BansheeEngine::DynLibManager::unload "DynLibManager::unload" - Unloads a previously loaded library.

Once the library is loaded you can use the @ref BansheeEngine::DynLib "DynLib" object, and its @ref BansheeEngine::DynLib::getSymbol "DynLib::getSymbol" method to retrieve a function pointer within the dynamic library, and call into it. For example if we wanted to retrieve a function pointer for the loadPlugin method:

// Load library
DynLib* myLibrary = DynLibManager::instance().load("myPlugin");

// Retrieve function pointer (symbol)
typedef void* (*LoadPluginFunc)();
LoadPluginFunc loadPluginFunc = (LoadPluginFunc)myLibrary->getSymbol("loadPlugin");

// Call the function
loadPluginFunc();

// Assuming we're done, unload the plugin
DynLibManager::instance().unload(myLibrary);

Testing {#utilities_o}

Implement @ref BansheeEngine::TestSuite "TestSuite" to set up unit tests for your application. To register new tests call @ref BS_ADD_TEST. Test is assumed to succeed unless either @ref BS_TEST_ASSERT or @ref BS_TEST_ASSERT_MSG are triggered.

class MyTestSuite : TestSuite
{
public:
	EditorTestSuite()
	{
		BS_ADD_TEST(MyTestSuite::myTest);
	}
	
private:
	void myTest()
	{
		BS_TEST_ASSERT_MSG(2 + 2 == 4, "Something really bad is going on.");
	}
};

To run all tests create a instance of the @ref BansheeEngine::TestSuite "TestSuite" and run it, like so:

SPtr<TestSuite> tests = MyTestSuite::create<MyTestSuite>();
tests->run(ExceptionTestOutput());

When running the test we provide @ref BansheeEngine::ExceptionTestOutput "ExceptionTestOutput" which tells the test runner to terminate the application when a test fails. You can implement your own @ref BansheeEngine::TestOutput "TestOutput" to handle test failure more gracefully.

Allocators {#utilities_p}

Banshee allows you to allocate memory in various ways, so you can have fast memory allocations for many situations.

General {#utilities_p_a}

The most common memory allocation operations are new/delete or malloc/free. Banshee provides its own wrappers for these methods as @ref BansheeEngine::bs_new "bs_new"/@ref BansheeEngine::bs_delete "bs_delete" and @ref BansheeEngine::bs_alloc "bs_alloc"/@ref BansheeEngine::bs_free "bs_free". They provide the same functionality but make it possible for Banshee to track memory allocations which can be useful for profiling and debugging. You should always use them instead of the standard ones.

Use @ref BansheeEngine::bs_newN "bs_newN"/@ref BansheeEngine::bs_deleteN "bs_deleteN" to create and delete arrays of objects.

UINT8* buffer = (UINT8*)bs_alloc(1024); // Allocate a raw buffer of 1024 bytes.
Vector2* vector = bs_new<Vector2>(); // Allocate and construct a vector
Vector2** vectors = bs_newN<Vector2>(5); // Allocate an array of five vectors

// Free and destruct everything
bs_free(buffer);
bs_delete(vector);
bs_deleteN(vectors, 5);

Stack {#utilities_p_b}

Stack allocator allows you to allocate memory quickly, usually without a call to the OS memory manager, usually making the allocation only little more expensive than using the internal OS stack. It also allocates memory with zero fragmentation, which can be very important for large applications such as games. Whenever possible you should use this allocator instead of the general purpose allocator.

However it comes with a downside that it can only deallocate memory in the opposite order it was allocated. This usually only makes it suitable for temporary allocations within a single method, where you can guarantee the proper order.

Use @ref BansheeEngine::bs_stack_alloc "bs_stack_alloc" / @ref BansheeEngine::bs_stack_free "bs_stack_free" and @ref BansheeEngine::bs_stack_new "bs_stack_new" / @ref BansheeEngine::bs_stack_delete "bs_stack_delete" to allocate/free memory using the stack allocator.

For example:

UINT8* buffer = bs_stack_alloc(1024);
... do something with buffer ...
UINT8* buffer2 = bs_stack_alloc(512);
... do something with buffer2 ...
bs_stack_free(buffer2); // Must free buffer2 first!
bs_stack_free(buffer);

Frame {#utilities_p_c}

Frame allocator is very similar to the stack allocator and it provides the same benefits (it's also very fast and causes no fragmentation). However it has different memory deallocation restrictions which make it usable in more situations than a stack allocator, at the cost of using up more memory.

Frame allocator segments all allocated memory into "frames". These frames are stored in a stack-wise fashion, and must be deallocated in the opposite order they were allocated, similar to how the stack allocator works. The difference is that frame allocator is not able to free memory for individual objects, but only for entire frames.

This releases the restriction that memory must be freed in the order it was allocated, which makes the allocator usable in more situations, but it also means that a lot of memory might be wasted as unused memory will be kept until the entire frame is freed.

Use @ref BansheeEngine::bs_frame_alloc "bs_frame_alloc" / @ref BansheeEngine::bs_frame_free "bs_frame_free" or @ref BansheeEngine::bs_frame_new "bs_frame_new" / @ref BansheeEngine::bs_frame_delete "bs_frame_delete" to allocate/free memory using the frame allocator. Calls to @ref BansheeEngine::bs_frame_free "bs_frame_free" / @ref BansheeEngine::bs_frame_delete "bs_frame_delete" are required even through the frame allocator doesn't process individual deallocations, and this is used primarily for debug purposes.

Use @ref BansheeEngine::bs_frame_mark "bs_frame_mark" to start a new frame. All frame allocations should happen after this call. If you don't call @ref BansheeEngine::bs_frame_mark "bs_frame_mark" a global frame will be used. Once done with your calculations use @ref BansheeEngine::bs_frame_clear "bs_frame_clear" to free all memory in the current frame. The frames have to be released in opposite order they were created.

For example:

// Mark a new frame
bs_frame_mark();
UINT8* buffer = bs_frame_alloc(1024);
... do something with buffer ...
UINT8* buffer2 = bs_frame_alloc(512);
... do something with buffer2 ...
bs_frame_free(buffer); // Only does some checks in debug mode, doesn't actually free anything
bs_frame_free(buffer2); // Only does some checks in debug mode, doesn't actually free anything
bs_frame_clear(); // Frees memory for both buffers

You can also create your own frame allocators by constructing a @ref BansheeEngine::FrameAlloc "FrameAlloc" and calling memory management methods on it directly. This can allow you to use a frame allocator on a more global scope. For example if you are running some complex algorithm involving multiple classes you might create a frame allocator to be used throughout the algorithm, and then just free all the memory at once when the algorithm finishes.

You may also use frame allocator to allocate containers like @ref BansheeEngine::String "String", @ref BansheeEngine::Vector "Vector" or @ref BansheeEngine::Map "Map". Simply mark the frame as in the above example, and then use the following container alternatives: @ref BansheeEngine::String "FrameString", @ref BansheeEngine::FrameVector "FrameVector" or @ref BansheeEngine::FrameMap "FrameMap" (other container types also available). For example:

// Mark a new frame
bs_frame_mark();
{
	FrameVector<UINT8> vector;
	... populate the vector ... // No dynamic memory allocation cost as with a normal Vector
} // Block making sure the vector is deallocated before calling bs_frame_clear
bs_frame_clear(); // Frees memory for the vector

Static {#utilities_p_d}

@ref BansheeEngine::StaticAlloc "Static allocator" is the only specialized type of allocator that is used for permanent allocations. It allows you to pre-allocate a static buffer on the internal stack. It will then use internal stack memory until it runs out, after which it will use normal dynamic allocations. If you can predict a good static buffer size you can guarantee that most of your objects don't allocate any heap memory, while wasting minimum memory on the stack. This kind of allocator is mostly useful when you have many relatively small objects, each of which requires dynamic allocation of a different size.

An example:

class MyObj
{
	StaticAlloc<512> mAlloc; // Ensures that every instance of this object has 512 bytes pre-allocated
	UINT8* mData = nullptr;
	
	MyObj(int size)
	{
		// As long as size doesn't go over 512 bytes, no dynamic allocations will be made
		mData = mAlloc.alloc(size);
	}
	
	~MyObj()
	{
		mAlloc.free(mData);
	}
}

Shared pointers {#utilities_p_e}

Shared pointers are smart pointers that will automatically free memory when the last reference to the pointed memory goes out of scope. They're implemented as @ref BansheeEngine::SPtr "SPtr", which is just a wrapper for the standard C++ library std::shared_ptr. Use @ref BansheeEngine::bs_shared_ptr_new "bs_shared_ptr_new" to create a new shared pointer, or @ref BansheeEngine::bs_shared_ptr "bs_shared_ptr" to create one from an existing instance. The pointer memory is allocated and freed using the general allocator.

For example:

class MyClass() {};

// Create a shared pointer with a new instance of MyClass
SPtr<MyClass> myObj = bs_shared_ptr_new<MyClass>();

MyClass* myRawObj = bs_new<MyClass>();

// Create a shared pointer with an existing instance of MyClass
SPtr<MyClass> myObj2 = bs_shared_ptr(myRawObj);