Skip to content
Li, Xizhi edited this page Aug 22, 2017 · 14 revisions

Extending NPLRuntime With C++ Plugins

NPL provides three extensibility modes: (1) NPL scripting (2) Mono C# dll (3) C++ plugin interface. All of them can be used simultaneously. Please see BasicConcept for details.

C++ plugins

C++ plugins allows us to treat dll/so file as a single script file. We would only want to use it for performance critical tasks or functions that make heavy use of other third-party C/C++ libraries.

The following is a sample c++ plugin. https://github.com/LiXizhi/ParaCraftSDK/tree/master/samples/plugins/HelloWorldCppDll

#include "stdafx.h"
#include "HelloWorld.h"

/**
* Optional NPL includes, just in case you want to use some core functions see GetCoreInterface()
*/
#include "INPLRuntime.h"
#include "INPLRuntimeState.h"
#include "IParaEngineCore.h"
#include "IParaEngineApp.h"

using namespace ParaEngine;

#ifdef WIN32
#define CORE_EXPORT_DECL    __declspec(dllexport)
#else
#define CORE_EXPORT_DECL
#endif

// forward declare of exported functions. 
extern "C" {
	CORE_EXPORT_DECL const char* LibDescription();
	CORE_EXPORT_DECL int LibNumberClasses();
	CORE_EXPORT_DECL unsigned long LibVersion();
	CORE_EXPORT_DECL ParaEngine::ClassDescriptor* LibClassDesc(int i);
	CORE_EXPORT_DECL void LibInit();
	CORE_EXPORT_DECL void LibActivate(int nType, void* pVoid);
	CORE_EXPORT_DECL void LibInitParaEngine(ParaEngine::IParaEngineCore* pCoreInterface);
}
 
HINSTANCE Instance = NULL;

ClassDescriptor* HelloWorldPlugin_GetClassDesc();
typedef ClassDescriptor* (*GetClassDescMethod)();

GetClassDescMethod Plugins[] = 
{
	HelloWorldPlugin_GetClassDesc,
};

/** This has to be unique, change this id for each new plugin.
*/
#define HelloWorld_CLASS_ID Class_ID(0x2b905a29, 0x47b409af)

class HelloWorldPluginDesc:public ClassDescriptor
{
public:
	void* Create(bool loading = FALSE)
	{
		return new CHelloWorld();
	}

	const char* ClassName()
	{
		return "IHelloWorld";
	}

	SClass_ID SuperClassID()
	{
		return OBJECT_MODIFIER_CLASS_ID;
	}

	Class_ID ClassID()
	{
		return HelloWorld_CLASS_ID;
	}

	const char* Category() 
	{ 
		return "HelloWorld"; 
	}

	const char* InternalName() 
	{ 
		return "HelloWorld"; 
	}

	HINSTANCE HInstance() 
	{ 
		extern HINSTANCE Instance;
		return Instance; 
	}
};

ClassDescriptor* HelloWorldPlugin_GetClassDesc()
{
	static HelloWorldPluginDesc s_desc;
	return &s_desc;
}

CORE_EXPORT_DECL const char* LibDescription()
{
	return "ParaEngine HelloWorld Ver 1.0.0";
}

CORE_EXPORT_DECL unsigned long LibVersion()
{
	return 1;
}

CORE_EXPORT_DECL int LibNumberClasses()
{
	return sizeof(Plugins)/sizeof(Plugins[0]);
}

CORE_EXPORT_DECL ClassDescriptor* LibClassDesc(int i)
{
	if (i < LibNumberClasses() && Plugins[i])
	{
		return Plugins[i]();
	}
	else
	{
		return NULL;
	}
}

ParaEngine::IParaEngineCore* g_pCoreInterface = NULL;
ParaEngine::IParaEngineCore* GetCoreInterface()
{
	return g_pCoreInterface;
}

CORE_EXPORT_DECL void LibInitParaEngine(IParaEngineCore* pCoreInterface)
{
	g_pCoreInterface = pCoreInterface;
}

CORE_EXPORT_DECL void LibInit()
{
}

#ifdef WIN32
BOOL WINAPI DllMain(HINSTANCE hinstDLL,ULONG fdwReason,LPVOID lpvReserved)
#else
void __attribute__ ((constructor)) DllMain()
#endif
{
	// TODO: dll start up code here
#ifdef WIN32
	Instance = hinstDLL;				// Hang on to this DLL's instance handle.
	return (TRUE);
#endif
}

extern "C" {
	/** this is an example of c function calling NPL core interface */
	void WriteLog(const char* str) {
		if(GetCoreInterface())
			GetCoreInterface()->GetAppInterface()->WriteToLog(str);
	}
}

/** this is the main activate function to be called. Test with 
	NPL.activate("this_file.dll", msg); 
or with synchronous invocation, use
	NPL.call("temp/HelloWorldPlugin.dll", {cmd=abc}); 
	echo(msg);
*/
CORE_EXPORT_DECL void LibActivate(int nType, void* pVoid)
{
	if(nType == ParaEngine::PluginActType_STATE)
	{
		NPL::INPLRuntimeState* pState = (NPL::INPLRuntimeState*)pVoid;
		const char* sMsg = pState->GetCurrentMsg();
		int nMsgLength = pState->GetCurrentMsgLength();

		NPLInterface::NPLObjectProxy input_msg = NPLInterface::NPLHelper::MsgStringToNPLTable(sMsg);
		const std::string& sCmd = input_msg["cmd"];
		if(sCmd == "hello" || true)
		{
			NPLInterface::NPLObjectProxy output_msg;
			output_msg["succeed"] = "true";
			output_msg["sample_number_output"] = (double)(1234567);
			output_msg["result"] = "hello world!";

			std::string output;
			NPLInterface::NPLHelper::NPLTableToString("msg", output_msg, output);
			// example output 1: return result using async callback to any thread to remote address
			pState->activate("script/test/echo.lua", output.c_str(), output.size());
			// example output 2: we can also write the result synchronously into a global msg variable.
			pState->call("", output.c_str(), output.size());
		}

		WriteLog("\n---------------------\nthis is called from c++ plugin\n");
	}
}

The most important function is LibActivate which is like NPL.this(function() end) in NPL script.

It is the function to be invoked when someone calls something like below:

NPL.activate("this_file.dll", {cmd="hello"})

One can also place dll or so file under sub directories of the working directory, such as NPL.activate("mod/this_file.dll", {cmd="hello"})

Note to Linux Developers

Under linux, the generated plugin file name is usually "libXXX.so", please copy this file to working directory and load using NPL.activate("XXX.dll", {cmd="hello"}) instead of the actual filename. NPLRuntime will automatically replace XXX.dll with libXXX.so when searching for the dll file. This way, your script code does not need to change when deploying to linux and windows platform.

Wrap Interface With Callback and Timeout

Communicating between C++ dll and NPL script with activation may be inconvenient. We can wrap our code with callbacks using the rpc class.

NPL.load("(gl)script/ide/System/Concurrent/rpc.lua");
local rpc = commonlib.gettable("System.Concurrent.Async.rpc");
rpc:new():init("Test.testRPC", function(self, msg) 
	LOG.std(nil, "info", "category", msg);
	msg.output=true; 
	ParaEngine.Sleep(1);
	return msg; 
end)
Test.testRPC:MakePublic();

-- now we can invoke it anywhere in any thread or remote address.
Test.testRPC("", {"input"}, function(err, msg) 
	assert(msg.output == true and msg[1] == "input")
	echo(msg);
end);

-- time out in 500ms
Test.testRPC("(worker1)", {"input"}, function(err, msg) 
	assert(err == "timeout" and msg==nil)
	echo(err);
end, 500);

Example With Async Callbacks and Timeout

Suppose we have developed a "temp/HelloWorldPlugin.dll" with following activation funciton in C++. Note, the function will return result using synchronous NPL.call method.

CORE_EXPORT_DECL void LibActivate(int nType, void* pVoid)
{
	if(nType == ParaEngine::PluginActType_STATE)
	{
		NPL::INPLRuntimeState* pState = (NPL::INPLRuntimeState*)pVoid;
		const char* sMsg = pState->GetCurrentMsg();
		int nMsgLength = pState->GetCurrentMsgLength();

		NPLInterface::NPLObjectProxy input_msg = NPLInterface::NPLHelper::MsgStringToNPLTable(sMsg);
		const std::string& sCmd = input_msg["cmd"];
		if(sCmd == "hello" || true)
		{
			NPLInterface::NPLObjectProxy output_msg;
			output_msg["succeed"] = "true";
			output_msg["sample_number_output"] = (double)(1234567);
			output_msg["result"] = "hello world!";

			std::string output;
			NPLInterface::NPLHelper::NPLTableToString("msg", output_msg, output);
			// example output: write the result synchronously into a global msg variable.
			pState->call("", output.c_str(), output.size());
		}
		WriteLog("\n---------------------\nthis is called from c++ plugin\n");
	}
}

On the NPL script side, we can wrap the C++ plugin with a function.

-- this is always a synchronous function
function TestHelloWorldPlugin(cmd)
   NPL.call("temp/HelloWorldPlugin.dll", {cmd=cmd})
   return msg.succeed and msg.result;
end
assert(TestHelloWorldPlugin("aaa") == "hello world!")

If the above function takes a long time to run, it may be a good idea to put it in another thread and wrap it with RPC. One can put the following code into a real script file and test. It will automatically generate the worker1 NPL thread to run the code instead of the calling thread. But pay attention,

  • you need to properly include dependent files in the file where the RPC is defined.
  • worker1 thread is persistent and created only once on the first invocation and never deleted. you can, therefore, use the same thread name for other RPC functions or even reusing other threads you created before.
NPL.load("(gl)script/ide/System/Concurrent/rpc.lua");
local rpc = commonlib.gettable("System.Concurrent.Async.rpc");

function TestHelloWorldPlugin(cmd)
   NPL.call("temp/HelloWorldPlugin.dll", {cmd=cmd})
   return msg.succeed and msg.result;
end

rpc:new():init("Test.HelloWorld", function(self, cmd) 
	return TestHelloWorldPlugin(cmd)
end)

-- the C++ function is run in `worker1` thread instead of the calling thread with 500ms timeout
Test.HelloWorld("(worker1)", "aaa", function(err, result) 
	assert(result == "hello world!")
	echo(result);
end, 500);
Clone this wiki locally