不依賴插件 給 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 的控制檯要怎麼做呢 ?

  1. C++ 裏,聲明 stdcall 類型的函數指針
  2. C++ 裏,聲明一個動態庫導出函數,參數值是 函數指針類型
  3. C# 中,做對應函數指針類型的 delegate ,和 DllImport 導入函數
  4. C# 中調用此函數,把 delegate 傳給 動態庫
  5. C++ 中保存函數指着呢
  6. 在希望調用的地方,直接調用此函數指針即可

如 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 調用

  1. 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()函數

  1. 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 時 ,即可斷點

總結

  1. Unity 集成 Lua ,依賴於 C# 的 InteropServices 機制 ,可以調用 C++ 寫的動態庫
  2. 又因爲 Lua 的開源 和 與 C++ 的完美配合,所以 C++ 能調用 Lua
  3. 因此, 打通了 C# -> 動態庫 -> C++ -> Lua 的調用通道
  4. C++ 在集成 Lua 後,可以給 Lua 註冊一些新的 C++ 函數,使得 Lua 能夠調用 C++
  5. 又因爲 C# 的 InteropServices 又可以給 C++ 傳遞函數地址 , 把 Delegate 作爲 C++ 裏的函數指針 傳給 C++ 動態庫
  6. 因此,只要 C++ 裏保存了 C# 的函數地址,C++ 就可以調用 C# 函數
  7. 由此打通了 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 項目的方法還沒看到。
待實現

發佈了160 篇原創文章 · 獲贊 77 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章