Embedding Lua in C# with P/Invoke

Started by
4 comments, last by Scourage 8 years, 1 month ago

Hey! For a project of mine, I've been wanting to embed Lua in C# so I can use it in my C# projects. I do not wish to use things like NLua or LuaInterface. I want to use P/Invoke to access the DLL functions. This is an easy way for me to update Lua to use Lua 5.3.2 etc.

I originally followed a tutorial: https://ttuxen.wordpress.com/2009/11/03/embedding-lua-in-dotnet/

So I pretty much followed it exactly except I followed some comments advice. I skipped the manifest part. I was confused as to what manifest file I needed and where it was. (Was it a Windows DLL manifest or should I create a manifest from the VS project? What?) Its not clear.

I ditched the def file and did this:

#define LUA_API in luaconf.h to read
#define LUA_API extern “C” __stdcall __declspec(dllexport)

I then copied the code example pretty much line by line then when running the console app I got: 'Unable to find an entry point named 'luaL_newstate' in DLL 'lua53.dll' Line 10 in the main function of the program.

I'm obviously doing something wrong and this is just in general chaotic for me. If anybody can lead me in the right direction to use P/Invoke with Lua and C#, it would be much appreciated.

Advertisement

I wanted the same thing a while ago and rolled my own for my game engine. If you want, please take a look at my Lua bindings for C# at: https://github.com/bholcomb/Lua- There is a thin layer on top of the pinvoke bindings, which covers about 99% of the Lua API. I wrote c# functions to implement the macro's in the lua header as well. If you only want the "pure" C api, then you should only need what is in the LuaDll.cs file. The LuaState and LuaObject are part of my thin wrapper.

It has the full lua 5.3.2 source built as part of the project, so you should only need to download, then run the build config (premake) then build the project to produce all the dlls you need. It is expecting lua5.3.dll in a x86 or x64 folder where you run your executable from. You can change how this works in the dllLoader class in the luaDll.cs file.

There is also a simple test console written in C# that uses the Lua#.dll to verify that it's working.

Please let me know if you find any bugs or have any questions.

cheers,

Bob


[size="3"]Halfway down the trail to Hell...

I wanted the same thing a while ago and rolled my own for my game engine. If you want, please take a look at my Lua bindings for C# at: https://github.com/bholcomb/Lua- There is a thin layer on top of the pinvoke bindings, which covers about 99% of the Lua API. I wrote c# functions to implement the macro's in the lua header as well. If you only want the "pure" C api, then you should only need what is in the LuaDll.cs file. The LuaState and LuaObject are part of my thin wrapper.

It has the full lua 5.3.2 source built as part of the project, so you should only need to download, then run the build config (premake) then build the project to produce all the dlls you need. It is expecting lua5.3.dll in a x86 or x64 folder where you run your executable from. You can change how this works in the dllLoader class in the luaDll.cs file.

There is also a simple test console written in C# that uses the Lua#.dll to verify that it's working.

Please let me know if you find any bugs or have any questions.

cheers,

Bob

Thanks for the detailed reply! I'm looking to do this myself for the experience. So if you can guide me a little bit i'd appreciate it.

The tutorial I followed required me to build the Lua DLL myself for a few reasons:

1. Calling Conventions (Changing it to __stdcall in VS settings)
2. Decorated functions (Using a .def file, but that solution didn't work for me)

3. C Manifest (It describes how its built into the DLL but needs a standalone manifest too)

Is this a non-issue for you? Did you modify how the DLL is built or is it a simple import files and compile? I'd like to know how you solved these potential issues.

Are the LuaObject and LuaState CS files just wrappers so users of the library can use those to simulate those objects rather than using IntPtrs all the time?

I'll fill in what I can. This may not be the best way to do this (and I'm sure there's bugs), but its been working for me so far.

In order to use p/invoke means you need to have a DLL that exports some functions. Don't worry about the calling convention, the P/Invoke API lets you set that for your DLL you're calling into. You also don't really need a def file, building your project as a DLL and having the right preprocessor flags set will export the symbols you want to bind to (at least in the Lua source). To build the Lua library as a DLL you simply need to build the lua library with the flag LUA_BUILD_AS_DLL, which if you look in luaconfig.h (line 235) to cause the symbol LUA_API to be defined as __declspec(dllexport). I didn't change any code and that is all that you need to do to build lua5.3.dll with its symbols exported.

Once you have a dll with symbols (or feel free to take mine, it's a vanilla 64bit Lua dll), you need to write a C# file that can call into it. I usually create my dll interface as a static class since each of these functions will be static functions. I simply went through the public Lua header and then copied the functions I want to be able to call to my C# file as comments so I have them there to reference and then started writing out the p/invoke function definition for each of the C functions.


      #region state manipulation
/*
LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud);
LUA_API void       (lua_close) (lua_State *L);
LUA_API lua_State *(lua_newthread) (lua_State *L);
LUA_API lua_CFunction (lua_atpanic) (lua_State *L, lua_CFunction panicf);
*/
      [DllImport(LUA_DLL, EntryPoint = "lua_newstate", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
      public static extern IntPtr lua_newstate(lua_Alloc f, IntPtr ud);


      [DllImport(LUA_DLL, EntryPoint = "lua_close", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
      public static extern void lua_close(IntPtr state);


      [DllImport(LUA_DLL, EntryPoint = "lua_newthread", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
      public static extern IntPtr lua_newthread(IntPtr state);


      [DllImport(LUA_DLL, EntryPoint = "lua_atpanic", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
      public static extern LuaCSFunction lua_atpanic(IntPtr state, LuaCSFunction panicf);


     #endregion

Here you can see each of the static functions bind to a function in the C dll. The DllImport attribute takes the dll filename, the function name as the entry point, the calling convention (see, no need to change how Lua is built, just change how pinvoke calls it), and a flag to suppress unmanaged security features for performance reasons. Most basic types are handled automatically. IntPtr is used for the pointer to the lua_State* since that's an opaque object (we don't access anything inside it ever). If you need to shuffle complex structures between C and C#, you should look into how marshalling and layout is done on the C# side. For callbacks, you simply need to define a C# delegate, but give it attributes that allow it to be called from C, and respecting the calling convention of your library. Here's the delegate definition for a typical function pushed into Lua from C#:


   //Delegate for functions passed to Lua as function pointers using cdecl vs the standard stdcall
   [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
   public delegate int LuaCSFunction(IntPtr luaState);

You also need to make sure any delegates you pass into Lua from C# don't get garbage collected. You do this by keeping a reference to the delegate around, maybe as a member of a class (see how I have myPrintFunction as a member of the LuaState below).

The dll filename is where I do something a little different. Its an idea blatenly stolen from another project (OVR C# bindings I believe). I preload the dll into the application before pinvoke looks for it. I can specific the architecture I want (x86 vs x64) by prepending a path to the dll name and then using LoadLibrary to bring that dll into the application space before any p/invoke functions are called. When P/Invoke tries to find the function it looks in the application's loaded dlls first (only by name, not path+name which is why this works), then going to the working directory and looking for dlls with matching names, then if one isn't found there looking in the system path. Pulling the DLL into the application space first can help prevent the wrong DLL being used (can happen when you have 2 versions of lua in your path, or getting a 32bit DLL when you need a 64bit DLL). You don't have to do this, just be sure that the DLL you want is in your application working directory or your path.

As I worked through the lua C api, I occasionally came across a macro that acted like a function, so I just implemented it as a function. Such as this:


/*#define luaL_dostring(L, s) \
   (luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))
*/


      public static int luaL_dostring(IntPtr L, String s)
      {
         int ret = luaL_loadstring(L, s);
         if (ret != 0)
         {
            return ret;
         }


         return lua_pcall(L, 0, LUA_MULTRET, 0);
      }

Once I got through the API (there were a couple of things I either stubbed out or didn't implement, but not many), I started adding some functionality on top of the raw dll calls. I created a LuaState object that internally has an IntPtr to a lua_State* object that is created from LuaDLL. This class provides access functions to get/set values by name/index as well as create tables and references to them. The class also provides some nice debug functions such as printing out the lua stack and printing out a lua table to a C# console.


public class LuaState
   {
      LuaCSFunction myPrintFuction;
      IntPtr myStatePtr;
      LuaObject myGlobalTable;


      public LuaState()
      {
         myStatePtr = LuaDLL.luaL_newstate();
         LuaDLL.luaL_openlibs(myStatePtr);


         LuaDLL.lua_getglobal(myStatePtr, "_G");
         myGlobalTable = new LuaObject(this, -1);


         myPrintFuction = new LuaCSFunction(print);


         LuaDLL.lua_pushcclosure(myStatePtr, myPrintFuction, 0);
         LuaDLL.lua_setglobal(myStatePtr, "print");


         printCallback = new PrintCallback(defaultPrint);
      }


...


}

The LuaObject provides an interface to any lua value whether or not it's a number, string, table, or function. It allows for iterating over a table, retrieving values or calling lua functions. It's just a nice abstraction.

Let me know if you have any questions.

cheers,

Bob


[size="3"]Halfway down the trail to Hell...

Thanks for the detailed information once again. I'm slowly working through everything. So my program has an error saying that the entry point 'lua_pcall' cant be found. The tutorial I followed in OP was based on Lua 5.1, but heres the culprit code I believe.


        [DllImport("lua53.dll", EntryPoint = "lua_pcall", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
        public static extern int lua_pcall(IntPtr lua_State, int nargs, int nresults, int errfunc);

        //simplify the execution of a Lua script
        public static int luaL_dostring(IntPtr lua_State, string s) {
            if (luaL_loadstring(lua_State, s) != 0)
                return 1;
            return lua_pcall(lua_State, 0, LUA_MULTRET, 0);
        }

So the problem is that in Lua 5.3 lua_pcall is a macro, not a function. You need to write a function that calls lua_pcallk (notice the K at the end). I wrote mine like this:


      [DllImport(LUA_DLL, EntryPoint = "lua_pcallk", CallingConvention = CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity]
      public static extern int lua_pcallk(IntPtr state, int nargs, int nresults, int errfunc, IntPtr ctx, LuaContinuationFunction k);


      public static int lua_pcall(IntPtr state, int nargs, int nresults, int errfunc)
      {
         return lua_pcallk(state, nargs, nresults, errfunc, IntPtr.Zero, null);
      }

There were quite a few changes in the API from 5.1 to 5.3.

cheers,

Bob


[size="3"]Halfway down the trail to Hell...

This topic is closed to new replies.

Advertisement