不依赖插件 给 Unity 项目接入 Lua
之前在公司给项目接入过 xLua .接入过程非常傻瓜.
又了解到 Unity 由于历史原因,有各种各样的 lua 接入插件。 slua,xlua,tolua 等等层出不穷。
如果是为了直接在 Unity 项目里使用 Lua,使用现成的插件肯定是最好的选择。如果是为了学习,就需要自己亲手实践一番
之前并不了解 unity 接入 lua 的原理 。
最近通过公司的项目,查阅看官方文档,了解到 Unity 能够使用 C# 代码调用 C++ 写的动态链接库,由此试验了一下从0开始接入 Lua
过程记录如下
#Unity 集成 Lua
文档参考Unity 官方文档 Working in Unity -> Advanced Development -> Plug-ins -> NataivePlug-ins 章节
https://docs.unity3d.com/Manual/NativePlugins.html
[file:///D:/UnityDocumentation_2019/en/Manual/PluginsForDesktop.html](file:///D:/UnityDocumentation_2019/en/Manual/PluginsForDesktop.html)
参考示例
https://github.com/Unity-Technologies/DesktopSamples
Unity 集成动态库
调用外部动态库的代码 ,要借助 InteropServices
因此, C# 中要 using System.Runtime.InteropServices;
这样可以使用 DllImport 标签
只能调用动态库中导出的函数,并且函数必须声明为 extern “C”
在 C# 里声明 C++ 函数,调用时,当作一个正常的 C#函数调用即可
using UnityEngine;
using System.Runtime.InteropServices;
class SomeScript : MonoBehaviour {
#if UNITY_IPHONE
// On __iOS__ plugins are statically linked into
// the executable, so we have to use __Internal as the
// library name.
[DllImport ("__Internal")]
#else
// Other platforms load plugins dynamically, so pass the name
// of the plugin's dynamic library.
[DllImport ("PluginName")]
#endif
private static extern float FooPluginFunction ();
void Awake () {
// Calls the FooPluginFunction inside the plugin
// And prints 5 to the console
print (FooPluginFunction ());
}
}
注意,要给每一个被导出的 C++ 函数加上 DllImport 声明
其中 “PluginName” 是 动态库文件的名字
编写动态库
C# 中,只能调用 C++ 动态库中导出的函数,而不能使用导出的 C++ 类,并且被导出的函数必须声明为 extern “C”
下面例子中,只在 C++ 代码里导出了 函数。希望暴露出来的 LuaMachine 类并没有导出,而是通过函数的形式暴露给 C#
例 Source.h
#pragma once
#ifdef AYY_LUA_BIND_EXPORT
#define AYY_LUA_BIND_API __declspec(dllexport)
#else
#define AYY_LUA_BIND_API __declspec(dllimport)
#endif
extern "C" AYY_LUA_BIND_API void launchLua();
extern "C" AYY_LUA_BIND_API void callLuaFunc();
extern "C" AYY_LUA_BIND_API void executeString(const char* luaCode);
typedef void(__stdcall* CallFunc) (const char* info);
extern "C" AYY_LUA_BIND_API void registerLogHandler(CallFunc func);
void printLog(const char* log);
Source.cpp
#include "../header/Source.h"
#include <stdio.h>
#include "../header/LuaMachine.h"
LuaMachine* machine = nullptr;
CallFunc logHandler = nullptr;
void launchLua()
{
machine = new LuaMachine();
printLog("launchLua\n");
machine->init();
}
void callLuaFunc()
{
if (machine != nullptr)
{
printLog("call lua func,machine NOT null\n");
}
else
{
printLog("call lua func,machine is NULL\n");
}
}
void executeString(const char* luaCode)
{
machine->executeString(luaCode);
}
void registerLogHandler(CallFunc func)
{
logHandler = func;
}
void printLog(const char* log)
{
if (logHandler != nullptr)
{
logHandler(log);
}
{
printf("%s\n",log);
}
}
这里再看一下官方示例的 Plugin.cpp .
官方示例里, _MSC_VER 下 把 EXPORT_API 定义为 __declspec(dllexport)
其他平台 没有使用 宏
#if _MSC_VER // this is defined when compiling with Visual Studio
#define EXPORT_API __declspec(dllexport) // Visual Studio needs annotating exported functions with this
#else
#define EXPORT_API // XCode does not need annotating exported functions, so define is empty
#endif
// ------------------------------------------------------------------------
// Plugin itself
// Link following functions C-style (required for plugins)
extern "C"
{
// The functions we will call from Unity.
//
EXPORT_API const char* PrintHello(){
return "Hello";
}
EXPORT_API int PrintANumber(){
return 5;
}
EXPORT_API int AddTwoIntegers(int a, int b) {
return a + b;
}
EXPORT_API float AddTwoFloats(float a, float b) {
return a + b;
}
} // end of export C block
Windows dll
参考 MSDN 文档
在Visual Studio C++中创建 C/dll
依赖于 windows 平台的 __declspec(dllimport) 和 __declspec(dllexport) 关键字
注意 ! 自己在测试时,如果希望 dll 能用,必须让 VC 采用 x64 64位的编译选项来编译。
为了能够 “附加到进程” 断点调试,必须是 Debug
即: x64 debug 编译 dll
dll 必须放在 Assets/Plugins/x64/ 目录下
Android so
todo
Mac
todo
iOS
todo
C++ 动态库集成 Lua
todo
C# 调用 C++
iOS 给函数声明为 [DllImport ("__Internal")]
其他平台给函数声明为 [DllImport (“PluginName”)]
之后 和 C++ 动态库 函数声明一致,即可当作普通的 C# 函数调用
C++ 调用 C#
比如 Unity C# 中打印 Log 到控制台的方法是 Debug.Log(“log…”);
而在 C++ 的 DLL 中 直接 printf() 是无法 把 log 打印到 Unity 控制台的
如果希望 让 C++ 的 DLL 里,也能输出到 Unity 的控制台要怎么做呢 ?
- C++ 里,声明 stdcall 类型的函数指针
- C++ 里,声明一个动态库导出函数,参数值是 函数指针类型
- C# 中,做对应函数指针类型的 delegate ,和 DllImport 导入函数
- C# 中调用此函数,把 delegate 传给 动态库
- C++ 中保存函数指着呢
- 在希望调用的地方,直接调用此函数指针即可
如 C++ 中 声明函数指针,以及导出函数
Source.h
typedef void(__stdcall* CallFunc) (const char* info);
extern "C" AYY_LUA_BIND_API void registerLogHandler(CallFunc func);
Source.cpp 实现里,将此函数指针保留
CallFunc logHandler = nullptr;
void registerLogHandler(CallFunc func)
{
logHandler = func;
}
C# 代码 TestDynamic.cs 里,声明对应的 Delegate 和 DllImport 函数
delegate void LogFuncDelegate(string log);
[DllImport(DLIB_NAME)]
extern public static void registerLogHandler(IntPtr func);
注意,这里的函数指针的参数类型,是 System.IntPtr
并编写调用 Debug.Log() 的函数
static void LogFunc(string log)
{
Debug.Log(log);
}
将此函数作为 delegate 实例,调用 DllImport 函数作参数
LogFuncDelegate logFunc = new LogFuncDelegate(TestDynamicLib.LogFunc);
registerLogHandler(Marshal.GetFunctionPointerForDelegate(logFunc));
注意,将 delegate 转换为 IntPtr 的方法是 Marshal.GetFunctionPointerForDelegate()
经过这样一番设置,在 C++ 动态库代码里,即可调用保存的函数指针 logHandler 来达到调用 C# 代码的目的
C# 传递 Lua 源码并执行
将 Lua 脚本代码放在 StreamingAssets 目录下 , 比如放在 Assets/StreamingAssets/Lua/main.lua
print("This is main.lua");
local counter = 0;
for i = 1,3 do
print(tostring(i));
counter = counter + i;
end
print("counter:" .. tostring(counter));
print(debug.traceback());
local function testFunc()
print(debug.traceback());
end
testFunc();
print("main.lua file end");
在 C# 中获取 lua 文件内容
string filePath = Application.streamingAssetsPath + "/Lua/" + fileName;
string code = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
把文件内容 ,当作 string 发给 cpp 动态库
[DllImport(DLIB_NAME)]
extern public static void executeString(string luaCode);
...
executeString(code);
cpp 动态库里面,接受到 const char* luaCode,当作字符串 调用 luaL_dostring
void LuaMachine::executeString(const char* luaCode)
{
luaL_dostring(m_state, luaCode);
}
从输出结果里可以看到,即使把 lua code 当作 字符串,调用了 luaL_dostring() ,lua 代码里的 debug.traceback() 依然能够正确的反应出行号
暴露 C# 函数 给 Lua 调用
-
C++ 给 Lua 注册新增的函数
lua_pushcfunction(this->m_state, lua_hookPrint);
lua_setglobal(m_state, “hookprint”);
lua_pushcfunction(this->m_state, lua_hookRequire);
lua_setglobal(m_state, “hookrequire”);int LuaMachine::lua_hookRequire(lua_State* L)
{
const char* str = lua_tostring(L, 1);
if (requireHandler != nullptr)
{
requireHandler(str);
}
return 0;
}
这样 Lua 只要调用 hookrequire("") ,即可走到 C++ 代码里面的 lua_hookRequire()函数
- C# 给C++ 传递函数指针,C++ 中保留函数指针
C++ Source.h
typedef void(__stdcall* RequireFunc) (const char* luaPath);
extern "C" AYY_LUA_BIND_API void registerRequireHandler(RequireFunc func);
Source.cpp
RequireFunc requireHandler = nullptr;
…
void registerRequireHandler(RequireFunc func)
{
requireHandler = func;
}
C#
delegate void RequireFuncDelegate(string luaPath);
...
RequireFuncDelegate requireFunc = new RequireFuncDelegate(TestDynamicLib.CSharp_Export_Require);
registerRequireHandler(Marshal.GetFunctionPointerForDelegate(requireFunc));
...
static void CSharp_Export_Require(string luaPath)
{
TestDynamicLib.instance.LuaDoFile(luaPath + ".lua");
}
这样 C++ 里调用 requireHandler() 即可调用到 C# 里的代码 CSharp_Export_Require()
这样,在 C++ 中 ,把 函数指针 的调用,放到 新注册的 Lua 函数里
int LuaMachine::lua_hookRequire(lua_State* L)
{
const char* str = lua_tostring(L, 1);
if (requireHandler != nullptr)
{
requireHandler(str);
}
return 0;
}
这样就完成了 Lua 调用 C# 的全过程
测试用的 Lua 代码
Assets/StreamingAssets/Lua/main.lua
print("This is main.lua");
local counter = 0;
for i = 1,3 do
print(tostring(i));
counter = counter + i;
end
print("counter:" .. tostring(counter));
print(debug.traceback());
local function testFunc()
print(debug.traceback());
end
testFunc();
print("main.lua file end");
require("testmodule")
print("require end...");
require("testmodule")
print("require again");
print(tostring(tm.the_var));
print("11111111111111111");
print(tostring(varA));
print("22222222222222");
Assets/StreamingAssets/Lua/test.lua
print("This is test module");
local varA = 89417;
print(tostring(varA));
local testmodule = {
["the_var"] = varA;
}
--return testmodule;
_G.tm = testmodule;
注意,由于 C++ 里面采用 luaL_dostring() 的方式,直接执行 lua 源码,并且在 unity 里直接 require 也并不知道 路径如何填写。
这里,我魔改了 require 函数,在 C++ 里 lua state 初始化时,直接 hook 了 print 和 require
void LuaMachine::init()
{
m_state = luaL_newstate();
luaL_openlibs(m_state);
openCustomLib();
luaL_dostring(m_state, "print = hookprint;");
if (requireHandler != nullptr)
{
luaL_dostring(m_state, "require = hookrequire;");
}
}
测试时发现, 这样的 require 除了无法在文件尾部 return 之外,其他没有明显劣势。最终实现的机制 和项目里正在使用的机制类似 。
windows DLL 调试
VisualStudio 下 ,把 DLL 工程设置为启动项,“调试” -> “附加到进程” ,选择 Unity进程。
在 Unity 通过 C# 调用到 C++ DLL 时 ,即可断点
总结
- Unity 集成 Lua ,依赖于 C# 的 InteropServices 机制 ,可以调用 C++ 写的动态库
- 又因为 Lua 的开源 和 与 C++ 的完美配合,所以 C++ 能调用 Lua
- 因此, 打通了 C# -> 动态库 -> C++ -> Lua 的调用通道
- C++ 在集成 Lua 后,可以给 Lua 注册一些新的 C++ 函数,使得 Lua 能够调用 C++
- 又因为 C# 的 InteropServices 又可以给 C++ 传递函数地址 , 把 Delegate 作为 C++ 里的函数指针 传给 C++ 动态库
- 因此,只要 C++ 里保存了 C# 的函数地址,C++ 就可以调用 C# 函数
- 由此打通了 Lua -> C++ -> 函数指针 -> C# delegate 的调用通道
至此,完成了 C# 集成 Lua 的功能
全部代码
C++
Source.h
#pragma once
#ifdef AYY_LUA_BIND_EXPORT
#define AYY_LUA_BIND_API __declspec(dllexport)
#else
#define AYY_LUA_BIND_API __declspec(dllimport)
#endif
extern "C" AYY_LUA_BIND_API void launchLua();
extern "C" AYY_LUA_BIND_API void callLuaFunc();
extern "C" AYY_LUA_BIND_API void executeString(const char* luaCode);
typedef void(__stdcall* CallFunc) (const char* info);
extern "C" AYY_LUA_BIND_API void registerLogHandler(CallFunc func);
typedef void(__stdcall* RequireFunc) (const char* luaPath);
extern "C" AYY_LUA_BIND_API void registerRequireHandler(RequireFunc func);
void printLog(const char* log);
Source.cpp
#include "../header/Source.h"
#include <stdio.h>
#include "../header/LuaMachine.h"
LuaMachine* machine = nullptr;
CallFunc logHandler = nullptr;
RequireFunc requireHandler = nullptr;
void launchLua()
{
machine = new LuaMachine();
printLog("launchLua\n");
machine->init();
}
void callLuaFunc()
{
if (machine != nullptr)
{
printLog("call lua func,machine NOT null\n");
}
else
{
printLog("call lua func,machine is NULL\n");
}
}
void executeString(const char* luaCode)
{
machine->executeString(luaCode);
}
void registerLogHandler(CallFunc func)
{
logHandler = func;
}
void registerRequireHandler(RequireFunc func)
{
requireHandler = func;
}
void printLog(const char* log)
{
if (logHandler != nullptr)
{
logHandler(log);
}
{
printf("%s\n",log);
}
}
LuaMachine.h
#pragma once
extern "C"
{
#include "../lua-5.3.5/lua.h"
#include "../lua-5.3.5/lauxlib.h"
#include "../lua-5.3.5/lualib.h"
}
class LuaMachine
{
public:
LuaMachine();
~LuaMachine();
void init();
void executeString(const char* luaCode);
private:
void openCustomLib();
private:
static int lua_hookPrint(lua_State* L);
static int lua_hookRequire(lua_State* L);
private:
lua_State* m_state = nullptr;
};
LuaMachine.cpp
#include "../header/LuaMachine.h"
#include "../header/Source.h"
extern void printLog(const char* log);
extern RequireFunc requireHandler;
LuaMachine::LuaMachine()
{
}
LuaMachine::~LuaMachine()
{
if (m_state != nullptr)
{
lua_close(m_state);
}
}
void LuaMachine::init()
{
m_state = luaL_newstate();
luaL_openlibs(m_state);
openCustomLib();
luaL_dostring(m_state, "print = hookprint;");
if (requireHandler != nullptr)
{
luaL_dostring(m_state, "require = hookrequire;");
}
}
void LuaMachine::executeString(const char* luaCode)
{
luaL_dostring(m_state, luaCode);
}
void LuaMachine::openCustomLib()
{
lua_pushcfunction(this->m_state, lua_hookPrint);
lua_setglobal(m_state, "hookprint");
lua_pushcfunction(this->m_state, lua_hookRequire);
lua_setglobal(m_state, "hookrequire");
}
int LuaMachine::lua_hookPrint(lua_State* L)
{
const char* str = lua_tostring(L, 1);
printLog(str);
return 0;
}
int LuaMachine::lua_hookRequire(lua_State* L)
{
const char* str = lua_tostring(L, 1);
if (requireHandler != nullptr)
{
requireHandler(str);
}
return 0;
}
C#
TestsDynamicLib.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using System;
using System.IO;
public class TestDynamicLib : MonoBehaviour
{
#if UNITY_IPHONE
[DllImport("__Internal")]
public const string DLIB_NAME = "__Internal";
#else
public const string DLIB_NAME = "ayyluabind";
#endif
static TestDynamicLib instance = null;
//private static extern float FooPluginFunction();
[DllImport(DLIB_NAME)]
extern public static void launchLua();
[DllImport(DLIB_NAME)]
extern public static void callLuaFunc();
[DllImport(DLIB_NAME)]
extern public static void executeString(string luaCode);
[DllImport(DLIB_NAME)]
extern public static void registerLogHandler(IntPtr func);
[DllImport(DLIB_NAME)]
extern public static void registerRequireHandler(IntPtr func);
delegate void LogFuncDelegate(string log);
delegate void RequireFuncDelegate(string luaPath);
// Start is called before the first frame update
void Start()
{
Debug.Assert(TestDynamicLib.instance == null);
if (TestDynamicLib.instance != null)
{
Destroy(gameObject);
return;
}
TestDynamicLib.instance = this;
// register c# function to dynamic lib
LogFuncDelegate logFunc = new LogFuncDelegate(TestDynamicLib.CSharp_Export_LogFunc);
registerLogHandler(Marshal.GetFunctionPointerForDelegate(logFunc));
RequireFuncDelegate requireFunc = new RequireFuncDelegate(TestDynamicLib.CSharp_Export_Require);
registerRequireHandler(Marshal.GetFunctionPointerForDelegate(requireFunc));
// launch lua
launchLua();
callLuaFunc();
executeString("print(\"string from c# lua code!\");");
Debug.Log(Application.persistentDataPath);
Debug.Log(Application.streamingAssetsPath);
Debug.Log(Application.dataPath);
Debug.Log("-------------");
LuaDoFile("main.lua");
}
// Update is called once per frame
void Update()
{
}
private void LuaDoFile(string fileName)
{
string filePath = Application.streamingAssetsPath + "/Lua/" + fileName;
Debug.Assert(File.Exists(filePath));
string code = File.ReadAllText(filePath, System.Text.Encoding.UTF8);
executeString(code);
}
static void CSharp_Export_LogFunc(string log)
{
Debug.Log(log);
}
static void CSharp_Export_Require(string luaPath)
{
TestDynamicLib.instance.LuaDoFile(luaPath + ".lua");
}
}
Lua
main.lua
print("This is main.lua");
local counter = 0;
for i = 1,3 do
print(tostring(i));
counter = counter + i;
end
print("counter:" .. tostring(counter));
print(debug.traceback());
local function testFunc()
print(debug.traceback());
end
testFunc();
print("main.lua file end");
require("testmodule")
print("require end...");
require("testmodule")
print("require again");
print(tostring(tm.the_var));
print("11111111111111111");
print(tostring(varA));
print("22222222222222");
test.lua
print("This is test module");
local varA = 89417;
print(tostring(varA));
local testmodule = {
["the_var"] = varA;
}
--return testmodule;
_G.tm = testmodule;
编译 Android 动态库 ,Mac 动态库,以及整合进 iOS 项目的方法还没看到。
待实现
下一篇: 2018-05-15适配机型