ShiVa Lua unlocked Pt.2: Editor DLL modules

Modules for the ShiVa 2.0 Editor are written in Lua (logic) and XML (UI). With far over 2000 Editor API functions and constants, you can write very sophisticated modules with the Editor Lua API alone. Although from time to time, you might run into C/C++ code that does exactly what you need for your ShiVa module. Wouldn't it be nice if you could just integrate that code into the editor without converting everything into Lua? You actually can! And it's not as difficult as you may think.

Lua SDK

The ShiVa 2.0 editor (currently) ships with the Lua 5.2.3 interpreter. In order to write DLLs for the editor, you need to download the headers and libraries matching ShiVa's interpreter. Luckily, all required packages are available on SourceForge at projects/luabinaries.

We will need the header files (include/*.h/hpp) as well as the 32/64-bit static libraries (.lib). Using the static lib is not ideal for maintenance and code duplication reasons, but very easy to do, so we will link against the .lib for this tutorial. If you are planning on writing many modules for ShiVa, you should consider linking against the Lua dll instead.

Setting up Visual Studio

Open Visual Studio 2012+ and create a new project. We recommend using the Win32 template, since it allows you to communicate with the "traditional" Windows API and UI easily, which will help us later in the tutorial when we are going to throw Win32 MessageBoxes up on the screen for debugging purposes.

win32proj

Copy your header and lib files into the project directory tree. We recommend a folder structure like this:

projectroot/luaheaders/x86/*.h
projectroot/luaheaders/x64/*.h
projectroot/lualibs/x86/lua52.lib
projectroot/lualibs/x64/lua52.lib

Copying the files is not enough. You need to tell Visual Studio about the location of the headers (Project Property Pages -> C++ -> General -> Additional Include Directories) as well as the lib path (Project Property Pages -> Linker -> General -> Additional Library Directories) and lib name (Project Property Pages -> Linker -> Input -> Additional Dependencies).

Anatomy of an Editor Plugin DLL

Switch to your main plugin CPP file in Visual Studio. The first thing you will need to do is declare all headers you would like to use, which are at least two: one for Windows, one for Lua.

  1. #include "stdafx.h" /* contains windows.h */
  2. #include "lua.hpp"

Since Lua is written in C and you will run into C/C++ name mangling if you blindly include the main lua header lua.h, include lua.hpp instead which wraps all Lua includes into an "extern C" block. You plugin will also need a main entrypoint. Like the includes, this function must be wrapped inside an "extern C" block. Additionally, it must be declared in a way so externa programs can view and access it:

  1. extern "C" {
  2.  
  3. int __declspec(dllexport) libinit(lua_State* L) {
  4. // register function callbacks, like so:
  5. // lua_register(L, "myFunc", lua_myFunc);
  6.  
  7. return 0;
  8. }
  9.  
  10. }

This function's only purpose is to register and name all C++ functions you would like to make accessible to Lua. You can easily do that through the macro lua_register().

Function structure

Functions you would like to lua_register also have to follow a certain structure. For our first example, we will be writing a simple function that adds 2 numbers and returns the sum:

  1. local nSum = DLLcalulateSum(nArg1, nArg2)

This prototype translates into C++ in the following way:

  1. int lua_calcsum(lua_State* L) {
  2. auto num1 = luaL_checknumber(L, 1);
  3. auto num2 = luaL_checknumber(L, 2);
  4. auto sum = num1 + num2;
  5.  
  6. lua_pushnumber(L, sum);
  7. return 1;
  8. }

Every Lua-CPP function is of type INT and takes a single argument of type lua_State*. The arguments you have passed via Lua as well as the return value(s) are handled in the function body by dedicated lua_* functions and lualL_* macros.

Arguments are sequentially numbered, beginning with 1. To access the first number argument, we called luaL_checknumber(L, 1), and for the second luaL_checknumber(L, 2). If we had 3 arguments, the 3rd would be luaL_checknumber(L, 3), and so forth.

Returns are handled through various lua_push* functions. In our case, we will be pushing a single number via lua_pushnumber(). Since we are only returning one argument to Lua, the return value of the CPP function is 1. But Lua often makes use of multiple return values, like in this function:

  1. local hours, minutes, seconds = DLLtime()

You can easily push multiple return values through lua_push*, as long as you increase the return INT to match their number:

  1. int lua_multitime(lua_State* L) {
  2. auto t = time(0);
  3. struct tm now;
  4. localtime_s(&now, &t);
  5.  
  6. lua_pushnumber(L, now.tm_hour);
  7. lua_pushnumber(L, now.tm_min);
  8. lua_pushnumber(L, now.tm_sec);
  9.  
  10. return 3;
  11. }

As you can see, we are lua-numberpushing 3 values, which is reflected in the "return 3" for the CPP function.

Arguments and Returns

Argument lists and return values are very flexible with Lua. CPP on the other hand usually requires setting up custom types, function overloading or variadic templates to do the same thing. Luckily with the Lua SDK, it takes only a few lines of code to achieve the same thing in CPP. In the next function, we are going to accept an arbitrarily long list of list of numbers and add them together in a safe way using type checks:

  1. int lua_genericsafesum(lua_State* L) {
  2.  
  3. int n = lua_gettop(L); /* number of arguments */
  4. lua_Number sum = 0; /* store result here */
  5. int i; /* FOR counter */
  6. for (i = 1; i <= n; i++) {
  7. if (!lua_isnumber(L, i)) { /* make sure we only accept numbers */
  8. lua_pushstring(L, "incorrect argument");
  9. lua_error(L); /* stop interpreter, exit with error */
  10. }
  11. sum += lua_tonumber(L, i); /* convert arg to num and add to sum */
  12. }
  13. lua_pushnumber(L, sum); /* return overall sum */
  14.  
  15. return 1;
  16. }

Struct-like data can be worked with more easily when you return them as tables. Tables use the same lua_push* functions, although you need to push in key->value pairs and reset the stack pointer (lua_settable to -3) every time you push a new set. For convenience, you should move this repetitive task into its own function:

  1. void setfieldNumber(lua_State* L, const char *index, float value) {
  2. lua_pushstring(L, index);
  3. lua_pushnumber(L, value);
  4. lua_settable(L, -3);
  5. }
  6.  
  7. int lua_multitimeAssocTable(lua_State* L) {
  8. auto t = time(0);
  9. struct tm now;
  10. localtime_s(&now, &t);
  11.  
  12. lua_newtable(L);
  13. setfieldNumber(L, "h", now.tm_hour);
  14. setfieldNumber(L, "m", now.tm_min);
  15. setfieldNumber(L, "s", now.tm_sec);
  16.  
  17. return 1;
  18. }

Working with the Windows API

Thanks to the stdafx.h header, you have access to the full Win32 API at any time. Would you like to pop up a message box instead of logging to the console? No Problem! As long as you keep in mind that ShiVa uses UTF-8 strings and Windows uses Unicode 16 bit wide chars. If you forget to convert the encodings, you will get only nonsense text in Chinese characters.

  1. wchar_t * convertCharArrayToLPCWSTR(const char* charArray) {
  2. wchar_t* wString = new wchar_t[4096];
  3. MultiByteToWideChar(CP_ACP, 0, charArray, -1, wString, 4096);
  4. return wString;
  5. }
  6.  
  7. int lua_msgbox(lua_State* L) {
  8. const char* message = luaL_checkstring(L, 1);
  9. const char* caption = luaL_optstring(L, 2, "ShiVa 2.0 Editor");
  10. auto lmessage = convertCharArrayToLPCWSTR(message);
  11. auto lcaption = convertCharArrayToLPCWSTR(caption);
  12. int result = MessageBox(NULL, lmessage, lcaption, MB_OK);
  13.  
  14. lua_pushnumber(L, result);
  15.  
  16. delete lmessage, lcaption;
  17. return 1;
  18. }

lua_msgbox converts two input strings into wchars and constructs a WinAPI Messagebox with them. The second string (caption) is optional and has a default text in case only one argument is provided.

Registering Functions

Every CPP function needs to be registered in your startup C function. Choose a unique name (prefix) for all your Lua functions to avoid conflicts with built-in Lua functions as well as the huge ShiVa API.

  1. extern "C" {
  2.  
  3. int __declspec(dllexport) libinit(lua_State* L) {
  4. lua_register(L, "DLLmsgbox", lua_msgbox);
  5. lua_register(L, "DLLcalcsum", lua_calcsum);
  6. lua_register(L, "DLLmultitime", lua_multitime);
  7. lua_register(L, "DLLgenericsafesum", lua_genericsafesum);
  8. lua_register(L, "DLLmultitimeAT", lua_multitimeAssocTable);
  9.  
  10. return 0;
  11. }
  12.  
  13. }

Building

If you have the deprecated 32bit version of ShiVa 2.0 installed, you have to build the x86 target, while the standard version of ShiVa 2.0 requires a 64bit (x64) library. After choosing the correct target platform, set your configuration from Debug to Release and you are ready to build.

The compiled DLL can be stored in a number of places. The two most important are the ShiVa 2.0 install directory root, which makes the DLL available to all projects, and the currently opened project root, which restricts the DLL to the currently open project.

package.loadlib()

The Lua function package.loadlib is the easiest way to load your new DLL into ShiVa Lua. It takes two arguments, the name (or path) of the DLL as well as the name of the entry function, the one that registers all your CPP functions. The function returns two variables, the first one indicating failure/success and the second one contains an error message on failure.

  1. local libinit, libmsg = package.loadlib("LuaMsgBoxWin32.dll", "libinit")
  2. log.message("-- Loading DLL Start -------------------------")
  3. log.message(libinit) -- NIL if error, otherwise CFunc pointer
  4. log.message(libmsg) -- NIL if successful, otherwise error msg
  5. log.message("-- Loading DLL End ---------------------------")
  6. libinit()

baddllpath

Loading the DLL is not enough, you also need to initialize the main C function by simply putting a pair of parentheses behind its name: libinit(). Now all your declared functions will be available to you from Lua:

  1. DLLmsgbox("Hooray, it really worked!", "Did it work?")
  2.  
  3. log.message("Sum: " ..DLLcalcsum(3, 5) )
  4.  
  5. local h,m,s = DLLmultitime()
  6. log.message ( "Time: " ..h ..":" ..m ..":" ..s )
  7.  
  8. log.message("" ..DLLgenericsafesum(1,2,3,4,5))
  9. --log.message("" ..DLLgenericsafesum(4,5,6,"shiva",8,9)) -- typecheck in action
  10.  
  11. local att = DLLmultitimeAT()
  12. log.message ( "ATTime: " ..att["h"] ..":" ..att["m"] ..":" ..att["s"] )

Further reading

Naturally, this tutorial only gives you a brief overview of how a DLL module is structured. Needless to say, you can lua_push* more than numbers, strings and tables, you can accept different types of arguments, work with optional arguments, manipulate the Lua stack on a fine grained level, and include 3rd party SDKs into your binary module. If you are interested in the subject, we highly recommend the following sites:

lua.org/pil
lua.org/manual
acamara.es/blog/passing-variables-from-lua-5-2-to-c-and-vice-versa/
lua-users.org/wiki/CreatingBinaryExtensionModules

Please keep in mind that Lua is in active development and a lot has changed between 5.0 and 5.3. functions like loadlib have moved into the package.* namespace for instance, and arguments or returns for certain functions have changed as well. When in doubt, have a look at the Lua 5.2.3 headers and documentation.