原生实现C#与Lua相互调用方法(Unity3D可用)

竹里
竹里
发布时间:2026-10-16 | 5人浏览过
原创

本文详细介绍如何基于原生Lua 5.1.5源码,在Unity环境中通过手动编译动态链接库和编写C#封装层,实现C#与Lua脚本的相互调用。内容包含NDK编译安卓SO库的配置代码、C#使用DLLImport处理数据类型转换的注意事项,以及参数传递、函数回调的实际代码演示。

一、编译Lua动态链接库

第1步:编译Windows下使用的DLL文件

在Visual Studio(如VS2015)中创建一个空的动态链接库项目。将Lua 5.1.5的源码拷贝至项目中并添加引用。若需自定义扩展可保留默认文件,否则可删除。编译前需在项目属性中配置以下宏定义:

  • LUA_BUILD_AS_DLL
  • _CRT_SECURE_NO_WARNINGS

完成配置后,即可分别编译x86和x64平台的DLL动态库。

第2步:编译Android下使用的SO文件

Android平台需要使用NDK进行编译。需要创建 Application.mk 和 Android.mk 两个配置文件,放置在Lua源码 src 目录的上一级目录中。

1. Application.mk 配置内容:

# Application.mk
APP_PLATFORM = android-23
APP_ABI := armeabi-v7a arm64-v8a
APP_STL := stlport_shared

2. Android.mk 配置内容:

# Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
MY_FILES_PATH  :=  $(LOCAL_PATH)/src
MY_FILES_SUFFIX := %.c
MY_UN_INCLUDE := %lua.c %luac.c
# 递归遍历目录下的所有的文件
rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))
# 获取相应的源文件
MY_ALL_FILES := $(foreach src_path,$(MY_FILES_PATH), $(call rwildcard,$(src_path),*.*) )
MY_SRC_LIST  := $(filter $(MY_FILES_SUFFIX),$(MY_ALL_FILES))
MY_SRC_LIST  := $(filter-out $(MY_UN_INCLUDE),$(MY_SRC_LIST))
MY_SRC_LIST  := $(MY_SRC_LIST:$(LOCAL_PATH)/%=%)
LOCAL_SRC_FILES = $(MY_SRC_LIST)
#打印编译信息
$(warning 'src_list='$(LOCAL_SRC_FILES))
LOCAL_MODULE    := CSharpLua
LOCAL_LDLIBS += -ldl
LOCAL_CFLAGS := $(L_CFLGAS)
include $(BUILD_SHARED_LIBRARY)

3. 执行编译命令:

打开CMD命令行,进入放置mk文件的目录,执行以下NDK编译命令:

ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk

二、编写C#使用的API

第1步:动态链接库在Unity中的存放位置

在Unity项目的Assets目录下创建 Plugins 文件夹用于存放不同平台的DLL/SO库。路径结构如下:

  • Windows DLL:Assets/Plugins/x86 和 Assets/Plugins/x86_64
  • Android SO:Assets/Android/libs/arm64-v8a(对应NDK编译生成的路径)

第2步:编写C# API接口 [LuaDll.cs]

大部分Lua接口的动态库调用可直接使用 DllImport。注意传入参数中 char* 类型的处理方式。以下为常用函数的C#声明示例:

[DllImport("CSharpLua", EntryPoint = "luaL_newstate")]
public static extern IntPtr luaL_newstate();
[DllImport("CSharpLua", EntryPoint = "luaL_openlibs")]
public static extern void luaL_openlibs(IntPtr L);
[DllImport("CSharpLua", EntryPoint = "luaL_loadbuffer")]
public static extern int luaL_loadbuffer(IntPtr L, byte[] buff, uint size, string name);
[DllImport("CSharpLua", EntryPoint = "lua_call")]
public static extern void lua_call(IntPtr L, int nargs, int nresults);
[DllImport("CSharpLua", EntryPoint = "lua_pcall")]
public static extern int lua_pcall(IntPtr L, int nargs, int nresults, int errfunc);

第3步:需要注意的几个关键点

1. 返回 char* 的处理

不可直接使用 string 接收返回值,否则会导致崩溃。需要通过 IntPtr 中转并使用 Marshal.PtrToStringAnsi 转换:

[DllImport("CSharpLua", EntryPoint = "lua_tolstring")]
private static extern IntPtr _lua_tolstring(IntPtr L, int idx, ref uint size);
public static string lua_tolstring(IntPtr L, int idx, ref uint size)
{
    IntPtr buffer = _lua_tolstring(L, idx, ref size);
    return Marshal.PtrToStringAnsi(buffer);
}

2. C#函数传递给Lua使用(委托)

导出给Lua调用的C#函数必须使用 delegate 类型:

public delegate int LuaFunction(IntPtr L);
[DllImport("CSharpLua", EntryPoint = "lua_pushcclosure")]
public static extern void lua_pushcclosure(IntPtr L, LuaFunction func, int idx);
public static void lua_pushcfunction(IntPtr L, LuaFunction func)
{
   lua_pushcclosure(L, func, 0);
}

3. 宏定义的替代实现

Lua源码中的宏(如 lua_setglobal)无法直接在C#中使用,需手动封装:

[DllImport("CSharpLua", EntryPoint = "lua_getfield")]
public static extern void lua_getfield(IntPtr L, int idx, string s);
public static void lua_getglobal(IntPtr L, string s)
{
   lua_getfield(L, LUA_GLOBALSINDEX, s);
}
[DllImport("CSharpLua", EntryPoint = "lua_setfield")]
public static extern void lua_setfield(IntPtr L, int idx, string s);
public static void lua_setglobal(IntPtr L, string s)
{
   lua_setfield(L, LUA_GLOBALSINDEX, s);
}

4. UserData 对象传递

C#类实例对象传递给Lua时需转换为 IntPtr,Lua返回时再通过 IntPtr 转换回C#对象:

[DllImport("CSharpLua", EntryPoint = "lua_pushlightuserdata")]
public static extern void _lua_pushlightuserdata(IntPtr L, IntPtr p);
public static void lua_pushlightuserdata<T>(IntPtr L, T p)
{
    IntPtr obj = Marshal.GetIUnknownForObject(p);
    _lua_pushlightuserdata(L, obj);
}
[DllImport("CSharpLua", EntryPoint = "lua_touserdata")]
public static extern IntPtr _lua_touserdata(IntPtr L, int idx);
public static T lua_touserdata<T>(IntPtr L, int idx)
{
   IntPtr p = _lua_touserdata(L, idx);
   return (T)Marshal.GetObjectForIUnknown(p);
}

三、C#与Lua的相互调用举例

第1步:C#中创建Lua环境

IntPtr L = LuaDll.luaL_newstate();
LuaDll.luaL_openlibs(L);

第2步:加载Lua代码并执行,向Lua传递参数

加载资源中的Lua文本并执行函数:

var data = Resources.Load<TextAsset>(lua_file);
int rc = LuaDll.luaL_loadbuffer(L, data.bytes, (uint)data.bytes.Length, lua_file);
rc = LuaDll.lua_pcall(L, 0, 0, 0);

LuaDll.lua_getglobal(L, "main");
// 传递参数
LuaDll.lua_pushinteger(L, 3333);
LuaDll.lua_pushnumber(L, 3.3);
// 执行main方法
int i = LuaDll.lua_pcall(L, 2, 0, 0);

第3步:将C#函数提供给Lua使用

需使用静态方法,并在Android环境下建议加上 [MonoPInvokeCallback] 标记:

LuaDll.lua_pushcfunction(L, LuaPrint);
LuaDll.lua_setglobal(L, "print");

[MonoPInvokeCallback]
static int LuaPrint(IntPtr L)
{
  Debug.Log(".....");
  return 0;
}

第4步:Lua代码调用C#方法并提供回调

示例展示了如何将Lua注册的函数保存至 LUA_REGISTRYINDEX,并在C#事件触发时回调执行:

static int FindAndBind(IntPtr L)
{
   GameObject go = LuaDll.lua_touserdata<GameObject>(L, 1);
   string path = LuaDll.lua_tostring(L, 2);
   // 将lua的函数放到LUA_REGISTRYINDEX上
   int idx = LuaDll.luaL_refEx(L);
   Transform t = go.transform.Find(path);
   Button btn = t.GetComponent<Button>();
   btn.onClick.AddListener(delegate() {
     // 从LUA_REGISTRYINDEX栈获取lua的函数进行执行
     LuaDll.lua_rawgeti(L, LuaDll.LUA_REGISTRYINDEX, idx);
     LuaDll.lua_pcall(L, 0, 0, 0);
   });
   return 0;
}

四、总结

通过上述步骤,可以较为轻松地实现C#与Lua的原生交互。该方式灵活度高,可以直接替换不同版本的Lua源码进行编译。但对于大规模项目而言,由于Unity中的每个组件(如 GameObject、Transform 等)都需要手动编写导出的接口代码,工作量巨大且容易出错。对于更复杂的生产环境,建议参考tolua或xlua等成熟插件的实现思路,它们考虑了更多底层细节和易用性封装。 以上就是原生实现C#与Lua相互调用方法的详细内容,更多关于C#与Lua交互的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表