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. See CTR-SDK Terminology for a glossary of basic CTR-SDK terms.
This document sometimes puts content in colored boxes like the one below to indicate that it is sample source code.
nn::ro::Initialize(pCrs, crsSize);
Sample source code uses the following user-defined functions in order to make the code easier to understand. You must implement processing equivalent to these functions in your application.
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 is the first module that is executed when application execution begins.
ro: name, index, and offset.
ro
ro.
cro.rlt
edit
ARMCC linker.
xrl
uae
ARMCC linker.
ro library
If an application does not use dynamic modules, then all its executable code is collected into a single file. When an application starts, all 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, then 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, thus running 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 will reduce your application's memory footprint by the size of the other mode.
When your application is launched, it starts running after all of its static modules have been loaded into memory. Thus the time it takes from the application's launch 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.
ro FormatThe 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. See CTR-SDK Build System Manual (for DLLs) or Guide to Developing a Build System (for DLLs) for information about specifying export types.
throw an exception from one module and catch it in another.
ro format itself, the SDK provides a tool to support automation of imports and exports. When you use the SDK's build system, it uses this tool to perform imports and exports automatically. You can also use this tool to strip dead code automatically.
The limitations of the ro format are as follows.
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.
ro File StructureIf 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 will be /.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 things you need to do in order to call functions and reference variables across modules are to share headers and code the calls and references.
To read about the restrictions on dynamic modules, see Restrictions.
You must add extra code to use DLL features. See Special Uses for details.
See CTR-SDK Build System Manual (for DLLs) if you will be using the SDK's build system. If you will be developing 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 table below shows the differences between the various export types.
| Name | Index | Offset | |
|---|---|---|---|
| Size of referencing CRO file | Large | Small | Small |
| Size of exporting CRO file | Large | Small | 0 |
Time required by LoadModule | Long | Medium | Short |
| Getting pointer manually | Possible | Possible | Impossible |
| Creating referencing module first | Possible | Impossible | Impossible |
| 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, you will choose index or name, depending on your needs.
Static libraries can be embedded into dynamic modules. However, in addition to the ro restrictions , you should also note the following precaution:
Some variables require special treatment. For more information, see Handling Symbols with Multiple Definitions.
Other than symbols with multiple definitions, 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, so to prepare memory for use by DLLs and to load DLLs you need to implement everything with code in the application. For details, see Using DLLs.
In addition, you need to add extra code to use DLL features. See Special Uses for details.
See CTR-SDK Build System Manual (for DLLs) if you will be using the SDK's build system. If you will be developing your own build system, see Guide to Developing a Build System (for DLLs).
You can select the export type, so 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.
Symbols with multiple definitions 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 are merged appropriately by the linker during linking, and an adjustment is made so that all the symbols become one in the overall program.
ro
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 are permitted to contain the same symbol. As a result, an instance of the symbols for the elements listed above exists in each module that is referenced.
However, because usually symbols that represent variables among the elements listed above 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.
Processes for variables and elements other than variables are separated in ro in order to handle both situations. 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 above, 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 will occur 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 will be necessary for the following.
The definitions of symbols for the above are created in object files created from the source files that referenced those symbols. So, 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 by using the variable represented by the symbol.
NN_UTIL_REFER_SYMBOL can be used for this purpose.
For example, when a header such as the following is used, the conditions described above 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 in the dynamic modules will cause an error 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 list below 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 will perform up to step 4 when the application starts, and 11 and beyond when it exits. You will only perform steps 5 to 10 as appropriate when the application is running.
Below, each of these steps is described in detail.
ro Library
You must first load the CRS file into memory (e.g., 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);
There is a one-to-one relationship between CRS files and static modules; there can only be one of them per static module. Similarly, the CRS file of one static module will be different from the CRS file of another. Therefore, a single application will never have more than one CRS file, and applications never choose which CRS file to load.
You must first load the CRR file into memory (e.g., 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 will create a single CRR file containing the information for all CRO files that the application will use. It is therefore 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 (e.g., 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 it is possible to call functions and read/write 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 obtain 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/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 certain conditions, you can call LoadModule again for the CRO object in the freed memory area, and use it again. See Reusing CRO Objects for details.
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 as well as 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.
See CTR-SDK Build System Manual (for DLLs) or Guide to Developing a Build System (for DLLs) for information about including debugging information in a CRR object.
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 will be exported. If explicit exports are used in a dynamic module, dead-code stripping will not remove the symbols, even if they are not referenced in the module.
When variables are defined in multiple modules, processing is performed as if the variables are defined in static modules (see Handling of Symbols with Multiple Definitions). However, variables exported explicitly in any module will not be included in that processing. In short, they will be 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 will be handled as follows.
Note: If it is a C++ function, you must add extern "C" to the definition. In order to export it, you must do so explicitly.
nnroProlog
If you define a function with the name nnroProlog in a CRO file, then nnroProlog will be 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, then nnroProlog will be 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, then unresolved references from this file will be linked to this function. If you do so, nnroUnresolved will be 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 will stop working if references, once 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, then 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 and/or exports a large number of symbols. Conversely, if the module does not have much of this information, then 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)) ;
The code above made the freeSize portion of pFreeBuffer available.
Normally, it is not possible to reuse a CRO module you passed to nn::ro::LoadModule after you have called nn::ro::Module::Unload on it. However, it is possible to reuse them if the following conditions are met.
FIX_LEVEL_0
Below are some ways you can 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 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();
// It can be reloaded without reading it from memory again
pModule = nn::ro::LoadModule(pCro, croSize, pBuffer, sizeInfo.bufferSize);
pModule->DoInitialize();
ro format" and moved the information about this topic to this item.nnroUnresolved can be used in static modules.CTR-06-0201-002-G
CONFIDENTIAL