欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-18 08:00:35
...

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

相关标签: 工作日志