dynamic_libraries - ryzom/ryzomcore GitHub Wiki


title: Library Loading description: published: true date: 2023-03-01T05:17:15.117Z tags: editor: markdown dateCreated: 2022-03-07T10:55:52.730Z

NeL provides a set of classes and macros to ease the loading and initialization of library loading. This is a system that NeL refers to as "Pure NeL Libraries." The essence of this system is the CLibrary class and the INelLibrary interface. As a client developer (using a pure NeL library) you will use the CLibrary class extensively. As a plugin/module developer you will use the INelLibrary interface. Before you can use a plugin or module you must create it.

Pure NeL Library Creation

Your first step in creating a pure NeL library will be to create the library class that will be loaded. This class inherits from the interface INelLibrary and provides a handful of methods to the library loading mechanism. There are two important methods to be implemented on this class: onLibraryLoaded and onLibraryUnloaded. They both accept a bool for a parameter. This parameter (called firstTime and lastTime, respectively) specifies the order in which this class is loaded and unloaded. Finally there's an important macro that you will need to use called NLMISC_DECL_PURE_LIB - this does the hard work of exporting your library in a manner that the CLibrary class can access it. Here's an example of a pure NeL library:

class CMyLibrary : public INelLibrary
{
public:
	void onLibraryLoaded(bool firstTime)
	{
		// you definitely want to load up any data from the disk that this library will use when it is loaded
		// but you probably only want to do it once to save time.
		if(firstTime)
			loadDatafiles();
	}

	void onLibraryUnloaded(bool lastTime)
	{
		// you would only want to delete/unload data if you knew this library was no longer in use.
		if(lastTime)
			unloadDatafiles();
	}

	void loadDatafiles()
	{
		// this would load something from the disk.
	}

	void unloadDatafiles()
	{
		// maybe this would save out the data to the disk and then delete any allocated objects.
	}
};

NLMISC_DECL_PURE_LIB(CMyLibrary); The method onLibraryLoaded is called after each loading of the library. The pure NeL library system keeps track of how many places have requested this class to be loaded so that you can perform critical initial loading logic as well as any logic you may want to execute every time something requests this library. It is totally up to you as a plugin/library implementor. A common use of this is demonstrated in the sample above - the loading of any assets or allocation of any new objects necessary. There doesn't need to be any logic in this method, you could implement it as:

void onLibraryLoaded() {}

The method onLibraryUnloaded is called after each unloading/release of the library. The parameter "lastTime" will be true if this call is the last remaining reference to the library. You would commonly do things such as save out data, delete allocated objects and other cleanup things. Think of it as a library-level destructor. As with the onLibraryLoaded the system doesn't require you to put any logic in this method, simply implement it as shown above - with no logic.

Finally there's the NLMISC_DECL_PURE_LIB macro. You don't really need to know much about this other than that it must be issued in your implementation so that the CLibrary system can access the symbol to load your newly created INelLibrary implementation.

Pure NeL Library Loading

While you may never create a plugin/library using the INelLibrary interface explained above hopefully you will find need to load a plugin using this system. Loading a plugin using the CLibrary system is fairly easy. The system does provide you with a variety of useful utility methods as well. Before we get to the meat of loading a library we should talk about a couple of the utilities that are provided through the static public members of the CLibrary class

The two most important methods provided are makeLibName and addLibPath. The makeLibName method takes a library name (e.g. mymodule) and outputs a new string with a decorated NeL library name. This means that it will convert a library name into a NeL-compliant library filename. It decorates the name in accordance with the platform and compilation mode. On Windows in ReleaseDebug mode mymodule becomes "mymodule_rd.dll" and on Linux it becomes "libmymodule_rd.so." Because it is possible that you will load a library before you have had a chance to populate CPath with search paths the CLibrary method provides you with the addPath and addPaths methods so that you can provide it with information on how to find the dynamic library to load.

Now that we understand some of the basic utility functions we will create a new CLibrary handle and use the loadLibrary method to actually load a library. Before explaining how all of this works together an example might be helpful:

int main()
{
	// Add the "modules" directory to the list of paths to search for dynamic libraries
 	CLibrary::addPath("modules");

	// Create a new library handle.
	CLibrary myModule;

	// Now load the library. mymodule.
 	myModule.loadLibrary("mymodule", true, true, true)

	// Perform some logic here
	// ...

	// Now before we exit we should be good programmers and release the library.
	myModule.freeLibrary();

	return 0;
}

The loadLibrary method takes 4 parameters: the name of the library, whether to add NeL decoration, whether to use the library paths and finally whether this library handle "owns" the library being loaded.

bool loadLibrary(const std::string &libName, bool addNelDecoration, bool tryLibPath, bool ownership=true);

If you used makeLibName yourself then you would set the 2nd parameter to "false" so that it doesn't attempt to re-decorate your library name. These first two parameters are pretty self explanatory. The 3rd parameter tells the library handle whether to use the paths that you added using the addPath or addPaths methods. If this is true it will go through each path in the order added to the library handle. The final parameter, ownership, should always be true unless you are doing something very advanced. This specifies whether or not this handle owns the symbols that are loaded by this. You could potentially use this parameter to some advanced uses but it disables your ability to unload the library with this handle. This last point is very important - if you load a library specifying an ownership of false and then attempt to free the library your code will assert. Finally it will return true if it successfully loaded this library.

Internally the CLibrary system tracks whether or not the library that was loaded was a pure NeL library or not. While at first blush you may have assumed that this system was written specifically for pure NeL libraries it has the ability to dynamically load and unload non-NeL libraries but you lose a fair amount of functionality. It is still suggested to load any dynamic libraries within NeL applications with this system for consistency and to avoid openly calling LoadLibrary or dlopen directly.

When you have loaded a pure NeL library you can retrieve the library author's INelLibrary object. This is important if you know specifically what the library implementation is. Some library authors may expose a handful of public methods on the library that are useful to its implementors. See the following example:

#include "mymodulelib.h"

int main()
{
 	CLibrary myModule;
	myModule.loadLibrary("mymodule", true, true, true);

	CMyModuleLibrary *myModLib = dynamic_cast<CMyModuleLibrary *>(myModule.getNelLibraryInterface());
	myModLib->startupClassFactories();

	// Perform some "game" logic
	// ...

	myModule.freeLibrary();

	// WARNING: DO NOT TRY TO USE myModLib anymore!

	return 0;
}

Exposing Symbols

Having taken the time to load the library does you little good if you have no method of exposing functions from the DLL. In the following example we'll illustrate how you can create an interface to represent your library API, implement the logic, and then expose the ability to create these classes from outside code after loading the library. A couple key concepts to keep in mind is that this technique is based upon your creation function definition existing in your source (client) code and the implementation of this creation function existing within your DLL.

// main.cpp
#include "itool.h"
#include <nel/misc/dynloadlib.h>

int main()
{
	// Create a new library handle.
	CLibrary myModule;

	// Now load the library. mymodule.
 	myModule.loadLibrary("mymodule", true, true, true)

	// Using the exposed typedef that tells us what the creation function
	// looks like and the string that tells us what the name of the creation
	// function is in the library use CLibrary to retrieve that function.
	// Note: It is typically easiest to use a reinterpret_cast but not necessarily safe.
	ITOOL_CREATE_PROC createITool = reinterpret_cast<ITOOL_CREATE_PROC>(myModule.getSymbolAddress(ITOOL_CREATE_PROC_NAME));

	// Then use that function to create a new instance of an ITool object (which is
	// in reality a concrete instantiation of Tool based on the logic of the createion
	// function defined in tool.cpp.
	ITool tool = createITool();

	// Use your tool object to print out what kind of tool library we loaded!
	nlinfo("The tool library loaded was for a: %s", tool.getToolName().c_str();

	// Now before we exit we should be good programmers and release the library.
	myModule.freeLibrary();

	return 0;
}

// itool.h

// First define our API through a pure virtual class.
class ITool
{
public:
	virtual std::string getToolName()=0;
};

// Then provide some definitions as to what the creation symbol name
// will be and what the proc template looks like.
const char *ITOOL_CREATE_PROC_NAME = "createIToolInstance";
typedef ITool* (*ITOOL_CREATE_PROC)(void);

// tool.cpp
#include "itool.h"
#include <nel/misc/dynloadlib.h>

// Create a concrete implementation of the ITool API.
class Tool : public ITool
{
public:
	std::string getToolName() { return "wrench"; };
};

// Then implement the actual function that will be available to create
// instances of this Tool object. Note that the name of this function is
// identifical to the ITOOL_CREATE_PROC_NAME in the itool.h header - this
// is critically important.
extern "C"
{
	NL_LIB_EXPORT ITool *createIToolInstance(void)
	{
		return new Tool();
	}
};

At first this seems dauntingly complicated but in reality it's very simple. It's based on three premises: you have a pure virtual interface providing the API required for end-users of your library, that you provide a function to create concrete instances that implement this API and that you expose this function's name and function pointer typedef to users implementing your API. It's easiest to assume that you'll understand interface-based design and instead focus on the mechanics of exposing this library to users.

The first step is to define the function pointer such that it has the correct return value and arguments to create your new object. This typedef must exist in your header that your users will be including. If you included tool.h instead of itool.h you as a user would have a number of linking errors due to missing the sybmols for the Tool class. Function pointer typedefs are pretty simple, but important here. We provided a typedef that returned a pointer to a ITool object and required no arguments.

The next step is to name this function - you need to provide a string name, preferably in the form of a const char in your public header which is itool.h in this instance and finally you will to implement a function with this name using the interface defined by the function pointer - a function returning a pointer to an ITool object with no arguments named createIToolInstance. It is also interesting to note that we used the NL_LIB_EXPORT macro. In Unix compilers you do not need to specify anything to tell the compiler to expose the symbols in the shared library, however in Windows you will need to use __declspec(dllexport) and this is logic that NeL performs for you through this macro.

Finally when you're ready to use your new library you will use your CLibrary handle to retrieve the creation function symbol through it's string name and then call it to invoke it's logic, which in this case is to return a new Tool object. The call to getSymbolAddress in the example above and the cast of the resulting object demonstrates this functionality.

Library Versioning

If this is a library where changes to the API could occur and be shipped with code not recompiled against the import library you will more than likely want to implement library versioning. This is a technique in which you specify a version number in your concrete implementation and only increment it when you believe that functionality changes or API changes will break code using the former library version. This uses the exact same technique as the creation function example above with a small twist.

// itool.h
const uint32 interfaceVersionITool = 0x13;

// Then provide some definitions as to what the creation symbol name
// will be and what the proc template looks like.
const char *ITOOL_VERSION_PROC_NAME = "getIToolVersion";
typedef uint32 (*ITOOL_VERSION_PROC)(void);

// tool.cpp
#include "itool.h"
#include <nel/misc/dynloadlib.h>

const uint32 interfaceVersionTool = 0x13;

extern "C"
{
	NL_LIB_EXPORT uint32 getIToolVersion(void)
	{
		return interfaceVersionTool;
	}
};

This uses, as stated above, the same technique for exposing symbols as the creation system but we have a small wrinkle. The version is defined in itool.h - this is important to note because both the software implementing the library and the library itself are using this version. However if you changed the API, incremented the version, and then ran the new library with the old software (meaning, you changed the API but did not recompile the software using the API) it has the ability to compare the version the library is using versus the API version it's build to expect. So you could do a comparison when you load the library:

// main.cpp
#include "itool.h"
#include <nel/misc/dynloadlib.h>

int main()
{
	// Load my library and get the symbol for the version function.
	CLibrary myModule;
	myModule.loadLibrary("mymodule", true, true, true);
	ITOOL_VERSION_PROC_NAME itoolVersionProc = reinterpret_cast<ITOOL_VERSION_PROC>(myModule.getSymbolAddress(ITOOL_VERSION_PROC_NAME));
	if(itoolVersionProc != NULL ) // verify that we got the symbol before we try to use it.
	{
		// Check to see if it is too old.
		if(itoolVersionProc()<interfaceVersionITool)
			nlwarning("The version of the ITool API is too old.");
		// Check to see if it is new version
		else if(itoolVersionProc()>interfaceVersionITool)
			nlwarning("The version of the ITool API is newer than the one we build on.");
	}
}

Source

⚠️ **GitHub.com Fallback** ⚠️