不依赖插件 给 Unity 项目接入 Lua

三世轮回 提交于 2020-01-20 18:45:32

之前在公司给项目接入过 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 项目的方法还没看到。
待实现

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!