This document provides information about the DLL mechanism that the CTR-SDK provides.
Information about the following can be found in the referenced documentation.
This document assumes basic knowledge of CTR-SDK terminology. For a glossary of basic CTR-SDK terms, see the CTR-SDK Terminology.
Content in shaded boxes, as shown below, indicate a sequence to be entered on the command line.
nn::ro::Initialize(pCrs, crsSize);
Sample source code uses the following user-defined functions to make the code easier to understand. You must implement processing equivalent to these functions in your application.
s64 GetFileSize(const wchar_t* path)
void* AllocateMemory(size_t size, int alignment)
void LoadFile(void* pBuffer, const wchar_t* path, size_t size)
This section defines terminology relating to the DLL mechanism provided by the CTR-SDK.
The meanings and usage of terms used in this document and the CTR-SDK may differ from their general meanings and usage.
nnMain. This module is the first module that is executed when application execution begins.
ro: name, index, and offset.
cro.rlt
edit
ARMCC linker.
ARMCC linker.
nn::ro namespace section of the API provided by the CTR-SDK. It is a collection of functions for using DLLs.
If an application does not use dynamic modules, all of its executable code is collected into a single file. When an application starts, all of its executable code is loaded into memory and maintained there until the application exits. The executable code is never modified while the application is running. The executable code that is loaded upon startup is called the "static module."
Meanwhile, if the application uses dynamic modules, it can split up its executable code into multiple modules. When the application starts, only the static module is loaded into memory. The static module is then able to load the dynamic modules, and thus run executable code including dynamic modules that can be loaded into and released from memory during program execution.
You can reduce the amount of memory that your code uses by breaking up your application into multiple modules, and loading them into memory only when needed, freeing them afterward.
For example, you could put single-player mode and multiplayer mode into separate DLLs, and load just one of them depending on the play mode. This reduces your application's memory footprint by the size of the other mode.
When your application is started, it starts running after all of its static modules have been loaded into memory. The time it takes from the application's start instruction until nnMain starts executing is proportional to the size of the static modules. You can shorten the time it takes for your application's first screen to appear by putting just the minimum code needed to show the first screen in your static module, and putting the other portions of your application into DLLs, and loading them after the first screen appears.
The DLL mechanism provided by the CTR-SDK is called RO.
The RO format features the following differences from other DLL implementations.
offset, index, or name. Although you can select the format for each symbol individually, the SDK's build system only supports selection at the module level. For information about specifying export types, see CTR-SDK Build System Manual (for DLLs) or Guide to Developing a Build System (for DLLs).
throw an exception from one module and catch it in another.
The RO format has the following limitations.
The RO format has the following restrictions.
new, delete, and other language features implemented as internal function calls, such as integer division and 64-bit arithmetic.
weak symbols and other features that are not part of the C/C++ language standards.
In the RO format, you can make and use any static library (.a) as a DLL. But because of these restrictions, if you intend to use a static library as a DLL, be sure to confirm with the library's creator that it can be made and used as a DLL.
The static libraries (.a) provided in the SDK must not be made and used as DLLs. That is prohibited.
If you did not use DLLs, you would develop CTR applications by creating CXI and CCI files from AXF files. If you use DLLs, you must additionally create CRS, CRR, and CRO files.
A CRS file stores reference and export information for static modules. It is used to resolve symbols between DLLs. It does not contain executable code.
A CRR file contains CRO management information. It also contains information necessary for debugging DLLs.
CRR files have an important limitation: they must be stored on the ROM-FS, directly under the .crr directory, which is in turn directly under the ROM-FS root directory. (If the ROM-FS root directory is /, this directory is /.crr.) The .crr directory must not contain any file other than the CRR file.
A CRO file is a dynamic module. It stores executable code and information necessary to resolve symbols between the application's various modules.
There are no special requirements for the source code of a dynamic module, with the exception of a few limitations. You can create them in the same manner as static modules. As with ordinary code, the only actions you need to take to call functions and reference variables across modules are to share headers and code the calls and references.
For more information about the restrictions on dynamic modules, see Restrictions.
You must add extra code to use DLL features. For more information, see Special Uses.
If you intend to use the SDK's build system, see CTR-SDK Build System Manual (for DLLs). If you plan to develop your own build system, see Guide to Developing a Build System (for DLLs).
The RO format has three export types. You must select which export types to use when you create your CRO file. The following table shows the differences between the export types.
| Name | Index | Offset | |
|---|---|---|---|
| Size of the referencing CRO file | Large | Small | Small |
| Size of the exporting CRO file | Large | Small | 0 |
Time required by LoadModule | Long | Medium | Short |
| Getting the pointer manually | Possible | Possible | No |
| Creating the referencing module first | Possible | No | No |
| Build steps | Normal | Normal | Many |
Name has the greatest functionality, followed by index and then offset, and greater functionality comes at a correspondingly greater cost.
Normally, it is fine to choose offset as the export type. If you want to get pointers manually (using GetPointer) or use a DLL that functions as a library, choose index or name, depending on your needs.
Static libraries can be embedded into dynamic modules. However, in addition to the RO restrictions, also note the following caution:
Some variables require special treatment. For more information, see Handling for Overloaded Symbols.
Other than taking care of the overloaded symbols, there is nothing special that must be done to reference DLL functions and variables. You simply share headers and reference the normal way.
In the RO format, the management of DLLs is left entirely up to the application. To prepare memory for use by DLLs and to load DLLs you must implement everything with code in the application. For more information, see Using DLLs.
In addition, you must add extra code to use DLL features. For more information, see Special Uses.
If you intend to use the SDK's build system, see CTR-SDK Build System Manual (for DLLs). If you plan to develop your own build system, see Guide to Developing a Build System (for DLLs).
You can select the export type, so that the static module also exports symbols for the dynamic module.
Selecting export types for static modules is the same as selecting export types for dynamic modules. For more information, see the dynamic modules section.
Overloaded symbols require special treatment.
Usually, having multiple instances of the same symbol in a single program is not permitted in the C/C++ programming languages. However, for the following elements in C++, the same symbol can be included in multiple object files and used normally.
Symbols for these elements are merged appropriately by the linker during linking, and an adjustment is made so that all the symbols become one in the overall program.
Because the symbols are merged by a linker, they must be merged in each module. Also, because plug-in type use is supported in RO, multiple modules can contain the same symbol. As a result, an instance of the symbols for these elements exists in each module that is referenced.
However, because usually symbols that represent variables among these elements are used with the assumption that all references are made to a unique instance, unintended results may occur if different instances are referenced in each module.
To handle both situations, processes for variables and elements other than variables are separated in RO. Among symbols that are defined in multiple modules, for symbols that represent variables, an instance in the static module is always referenced, and all other symbols can be contained in every module.
As described previously, when symbols that represent variables are defined in multiple modules, RO assumes that those symbol definitions are in the static module and then references them in the static module.
If a definition that is referenced is not in the static module, an error occurs when linking the static module. In such cases, the code must be revised so that the static module contains a definition for the referenced symbol.
If multiple definitions are not created (intentionally or accidentally), support is necessary for the following.
The definitions of symbols for these elements are created in object files created from the source files that referenced those symbols. Reference these symbols from the static module source files to make sure that the definitions are contained in the static module. Often these symbols cannot be directly referenced. The definitions can be included in the static module by using indirect methods such as referencing the function using the target variable.
You can use NN_UTIL_REFER_SYMBOL for this purpose.
For example, when a header such as the following is used, the conditions described previously apply because ms_pInstance is a template class class member variable. If the static module is not using a class S at all and the S class is used in multiple dynamic modules, Singleton<S>::ms_pInstance causes an error in the static modules at build time.
Header
(Code from the very beginning.)
... (omitted) ...
template <class T>
class Singleton
{
public:
static T& GetInstance() { return *ms_pInstance; }
protected:
Singleton() { ms_pInstance = static_cast<T*>(this); }
~Singleton() { ms_pInstance = NULL; }
private:
static T* ms_pInstance;
};
template <class T> T* Singleton <T>::ms_pInstance = NULL;
class S : public Singleton<S>
{
};
... (omitted) ...
In this case, the following functions are added to source files and other files that include nnMain. These functions do not actually need to be called.
Any source files that are used in static modules.
(Code added for RO support.)
... (omitted) ...
void DummyReference()
{
NN_UTIL_REFER_SYMBOL(S::GetInstance);
}
The following list shows the sequence from loading a single CRO file, to using it, and then freeing all memory.
nn::ro::Initialize(crs)nn::ro::RegisterList(crr)nn::ro::LoadModule(cro)pModule->DoInitialize()pModule->DoFinalize()pModule->Unload()pRr->Unregister()nn::ro::Finalize()If you use multiple CRO files at the same time, simply repeat steps 5 to 7 as many times as necessary. If you want to load and use another CRO file after freeing the first, continue to step 10, and then return to step 5.
Normally, you perform up to step 4 when the application starts, and 11 and beyond when it exits. You only perform steps 5 to 10 as appropriate when the application is running.
The following sections describe each of these steps in detail.
ro Library
You must first load the CRS file into memory (such as from ROM-FS), and then call nn::ro::Initialize with this as the argument.
const wchar_t* crsPath = L"rom:/static.crs";
size_t crsSize = GetFileSize(crsPath);
void* pCrs = AllocateMemory(crsSize, 0x1000);
LoadFile(pCrs, crsPath, crsSize);
nn::ro::Initialize(pCrs, crsSize);
CRS files and static modules have a one-to-one relationship; there can only be one of them per static module. Similarly, the CRS file of one static module is different from the CRS file of another. A single application never has more than one CRS file, and applications never choose which CRS file to load.
You must first load the CRR file into memory (such as from ROM-FS), and then call nn::ro::RegisterList with this as the argument.
const wchar_t* crrPath = L"rom:/.crr/static.crr";
size_t crrSize = GetFileSize(crrPath);
void* pCrr = AllocateMemory(crrSize, 0x1000);
LoadFile(pCrr, crrPath, crrSize);
nn::ro::RegistrationList* pList = nn::ro::RegisterList(pCrr, crrSize);
A CRR file contains information about multiple CRO files. Before loading a CRO file, you must register the CRR file that contains the information about it. Ordinarily, you create a single CRR file containing the information for all CRO files that the application uses. It is normally sufficient to register just that one file. Use multiple CRR files if you want to also optimize the memory used by CRR files, or if you will be distributing additional programs.
Load the CRO file, which is the actual dynamic module. Load the CRO file into memory (such as from ROM-FS), and then pass that as an argument to nn::ro::LoadModule. You must pass to LoadModule a memory area for the buffer required separately by the CRO file. Pass the CRO object as an argument to nn::ro::GetSizeInfo to get the size of the buffer required by the CRO file.
After LoadModule succeeds, you must call nn::ro::Module::DoInitialize on the value returned by LoadModule before using the module.
const wchar_t* croPath = L"rom:/module1.cro"; size_t croSize = GetFileSize(croPath); void* pCro = AllocateMemory(croSize, 0x1000); LoadFile(pCro, croPath, croSize); nn::ro::SizeInfo sizeInfo; nn::ro::GetSizeInfo(&sizeInfo, pCro); void* pBuffer = AllocateMemory(sizeInfo.bufferSize, 8); nn::ro::Module* pModule = nn::ro::LoadModule( pCro, croSize, pBuffer, sizeInfo.bufferSize); pModule->DoInitialize();
DoInitialize builds global objects. If two modules reference each other in the global-objects building stage, first perform the steps up to LoadModule for each module, and then call DoInitialize after both calls to LoadModule complete.
You have now resolved the references to the CRO file, and can call functions and read and write the variables defined in it. However, this does not necessarily mean that all the references by the loaded CRO file to other modules have been resolved. The other CRO modules that this one references may not have been loaded yet, or the symbols that it references may not have been defined or exported at all.
You can use the nn::ro::Module::IsAllSymbolResolved function to determine whether all references to other modules by the CRO file you've loaded are resolved.
Although references are resolved automatically, you can also get pointers manually. To get a pointer to a function or variable manually, use nn::ro::GetPointer or nn::ro::Module::GetPointer.
// Get a pointer to a function manually, and call it. int (*pFunc)(int) = pModule->GetPointer<void (*)(int)>("TestFunc"); int ret = pFunc(0x1234); // Get a pointer to a variable manually, and read and write it. int& var = *pModule->GetPointer<int*>("g_Variable"); NN_LOG("%d\n", var); var = ret;
To release the memory used by a CRO object, call nn::ro::Module::DoFinalize and nn::ro::Module::Unload on the value returned by LoadModule.
pModule->DoFinalize(); pModule->Unload(); FreeMemory(pBuffer); FreeMemory(pCro);
Under some conditions, you can call LoadModule again for the CRO object in the freed memory area, and use it again. For more information, see Reusing CRO Objects.
To release the memory used by a CRR object, call nn::ro::RegistrationList::Unregister on the value returned by RegisterList.
pList->Unregister();
FreeMemory(pCrr);
To release the memory area used by a CRS object, call nn::ro::Finalize.
nn::ro::Finalize();
FreeMemory(pCrs);
Calling nn::ro::Finalize enables you to free the memory for the CRS object in addition to all CRR and CRO objects.
A CRO object can be used to debug source code in the debugger. You must include debugging information in the CRR object to debug source code.
For information about including debugging information in a CRR object, see CTR-SDK Build System Manual (for DLLs) and Guide to Developing a Build System (for DLLs).
Explicit imports and exports are mainly used in combination to enable use of plug-in type DLLs.
To explicitly import a symbol, add NN_DLL_IMPORT to the beginning of the function or variable declaration.
extern NN_DLL_IMPORT int g_Variable; extern NN_DLL_IMPORT void Function();
An explicitly imported symbol does not generate an error, even if this definition cannot be found at link time by the linker.
An explicitly exported symbol is not automatically imported, so you need to either explicitly import the symbol or get the pointer manually.
To explicitly export a symbol, add NN_DLL_EXPORT to the beginning of the function or variable declaration.
NN_DLL_EXPORT int g_Variable;
NN_DLL_EXPORT void Function()
{
... (omitted) ...
}
Explicitly exported symbols are exported. If explicit exports are used in a dynamic module, dead-code stripping does not remove the symbols, even if they are not referenced in the module.
Defining variables in multiple modules is equivalent to defining them in a static module (see Handling for Overloaded Symbols). However, variables exported explicitly in any module are not included. In short, they are treated as separate variables that have their own separate memory regions in each module.
Some of the names exported by a DLL are handled in a special way. There are three such names: nnroProlog, nnroEpilog, and nnroUnresolved. If a function with one of these names is defined in a DLL and exported, it is handled as follows.
Note: If it is a C++ function, you must add extern "C" to the definition. To export it, you must do so explicitly.
nnroProlog
If you define a function with the name nnroProlog in a CRO file, nnroProlog is called when nn::ro::Module::DoInitialize is called on that CRO file. This enables you to implement initialization unique to that DLL.
nnroProlog is called after DoInitialize has completed all initialization steps, including building global objects.
extern "C" NN_DLL_EXPORT void nnroProlog()
{
... (omitted) ...
}
nnroEpilog
If you define a function with the name nnroEpilog in a CRO file and export it, nnroProlog is called when nn::ro::Module::DoFinalize is called on that CRO file. This enables you to implement finalization unique to that DLL.
nnroEpilog is called before DoFinalize does any finalization, such as freeing global objects.
extern "C" NN_DLL_EXPORT void nnroEpilog()
{
... (omitted) ...
}
nnroUnresolved
If you define a function named nnroUnresolved in your CRO file and export it, unresolved references from this file are linked to this function. If you do so, nnroUnresolved are called when a function is called, and the DLL in which it is defined has not been loaded.
extern "C" NN_DLL_EXPORT void nnroUnresolved()
{
... (omitted) ...
}
This particular function can also be used in static modules. However, there is a limitation. It stops working if references, after they have been resolved, are reverted to the unresolved state.
In addition to executable code, a CRO file contains information about symbols referenced from other modules, and symbols exported to other modules. If you know that this information will not be needed after the CRO file is loaded, you can free it and use it for other purposes after LoadModule by specifying a fixLevel argument to nn::ro::LoadModule. This can greatly reduce your memory usage, particularly when the module references or exports a large number of symbols. Conversely, if the module does not have much of this information, freeing it may not increase the available memory size at all.
const wchar_t* croPath = L"rom:/module1.cro";
size_t croSize = GetFileSize(croPath);
void* pCro = AllocateMemory(croSize, 0x1000);
LoadFile(pCro, croPath, croSize);
nn::ro::SizeInfo sizeInfo;
nn::ro::GetSizeInfo(&sizeInfo, pCro);
void* pBuffer = AllocateMemory(sizeInfo.bufferSize, 8);
nn::ro::Module* pModule = nn::ro::LoadModule(
pCro, croSize, pBuffer, sizeInfo.bufferSize,
true, nn::ro::FIX_LEVEL_3);
void* pFreeBuffer = reinterpret_cast(sizeInfo.fix3End) ;
size_t freeSize = croSize - (sizeInfo.fix3End - reinterpret_cast(pCro)) ;
This code made the freeSize portion of pFreeBuffer available.
Normally, you cannot reuse a CRO module you passed to nn::ro::LoadModule after you have called nn::ro::Module::Unload on it. However, you can reuse them if the following conditions are met.
FIX_LEVEL_0.
The following practices enable you to satisfy condition 2 in your implementation:
nnroProlog function.
const wchar_t* croPath = L"rom:/module1.cro";
size_t croSize = GetFileSize(croPath);
void* pCro = AllocateMemory(croSize, 0x1000);
LoadFile(pCro, croPath, croSize);
nn::ro::SizeInfo sizeInfo;
nn::ro::GetSizeInfo(&sizeInfo, pCro);
void* pBuffer = AllocateMemory(sizeInfo.bufferSize, 8);
// Loaded the first time.
nn::ro::Module* pModule = nn::ro::LoadModule(
pCro, croSize, pBuffer, sizeInfo.bufferSize,
true, nn::ro::FIX_LEVEL_0);
pModule->DoInitialize();
... (use cro module) ...
// Free it.
pModule->DoFinalize();
pModule->Unload();
// You can reload it without reading it from memory again.
pModule = nn::ro::LoadModule(pCro, croSize, pBuffer, sizeInfo.bufferSize);
pModule->DoInitialize();
nnroUnresolved can be used in static modules.CTR-06-0201-002-I
CONFIDENTIAL