之前在公司給項目接入過 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 項目的方法還沒看到。
待實現