Lua 基礎之 Lua 程序

lua 調用 c 函數

lua 調用 c 函數與 c 調用 lua 函數一樣,都是通過 CAPI 和一個棧來實現,lua 調用 c 函數有以下幾個步驟
1. 在 c 代碼中定義函數
lau.h 中定義了要註冊到 lua 中的 c 函數的原型,雖然我測試的時候不使用這個原型也不會出錯,但還是要遵循規範比較好
int FunctionName(lua_State* L)
返回值表示此函數有幾個返回值,lua 執行完些函數之後根據這個返回值就知道需要從棧中取幾個返回值,而 lua_State* 參數使得 c 函數內可以使用 CAPI 來操作棧,與 lua 交互
c 函數的實現流程:
(1)從棧中取函數的參數。lua 調用 c 函數之前會把參數壓入棧中,每個 c 函數都有一個獨立的私有棧,所以函數第一個參數在棧中的索引是 1。
(2)實現函數功能。
(3)把返回值逐個壓入棧中,第一個返回值最先壓入,也就是在棧底;把結果壓入棧之前不用手動清除棧中原來的參數和函數,lua 會自動幫我們清除。
(4)返回一個整數表示該函數有幾個返回值。
2. 把 c 函數註冊到 lua 環境,在 lua 中創建一個全局變量保存這個函數
使用 lua_pushcfunction(L, c_fun_name) 把函數壓入棧中
使用 lua_setglobal(L, l_fun_name) 把函數賦給一個全局變量
3. 在 lua 程序中使用上面創建的全局變量了

main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

int sayHello(lua_State* L)
{
    printf("hello world in c!\n");
    return 0;
}

int l_sin(lua_State* L)
{
    //double angle = lua_tonumber(L, 1);
    double angle = luaL_checknumber(L, 1);
    lua_pushnumber(L, sin(angle));
    return 1;
}

int main(void)
{
    lua_State* L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, sayHello);
    lua_setglobal(L, "c_say_hello");

    lua_pushcfunction(L, l_sin);
    lua_setglobal(L, "c_sin");

    int error = luaL_loadfile(L, "main.lua") | lua_pcall(L, 0, 0, 0);
    if (error != 0)
    {
        printf("main.lua run error!\n");
    }

    system("pause");
    return 0;
}

加載 main.lua 之前先把兩個 c 函數 sayHellol_sin 註冊到 lua 環境中,然後再加載運行 main.lua 文件,這時 lua 環境中已經有兩個全局變量 c_say_helloc_sin 了,main.lua 中的代碼塊直接使用這兩個全局函數就不會找不到了

main.lua

print "hello world in lua!"

c_say_hello()

print("PI = 3.14")
local angle
repeat
    angle = tonumber(io.read())
    local s, c = c_sin(angle)
    print(string.format("sin(%g) = %.2f     cos(%g) = %.2f", angle, s, angle, c))
until angle == -1

c 模塊

我們已經知道了如何在執行 lua 代碼塊之前註冊 c 函數,但前面的程序還是一個 c 程序,只不過實現了 lua 調 c 而已。如果我們想要在使用 lua 解釋器來運行 lua 代碼時能夠讓 lua 調用 c 函數,那就得事先將 c 函數導出來。將要導出的 c 函數導出爲一個模塊,lua 代碼可以像加載 lua 模塊一樣加載這個模塊;lua 不僅能加載以 .lua 爲後綴的 lua 文件,還能加載以 .dll.so 爲後綴的二進制文件,我們就是要將 c 函數導出爲 .dll.so 的動態鏈接庫。

導出 c 函數模塊的流程(以 windows 下導出 dll 爲例):
1. 定義要導出給 lua 調用的 c 函數,加上 static 定義成私有的,因爲這些函數的註冊在模塊內部完成,不需要模塊外部訪問
2. 把這些 c 函數組織成一個數組,類型爲 const luaL_Reg[],同樣爲私有的
3. 定義一個外部函數,將所有 c 函數註冊到 lua 環境,這個函數是外部調用的接口,lua 通過這個函數來註冊上面組織的所有函數。爲了讓外部模塊能夠訪問到這個函數,函數除了要 定義成公有之外,還需要將函數導出,dll 導出函數的方式是在函數前面加上 __declspec(dllexport)
註冊 c 函數:
lua 5.1: luaL_register(L, "mylib", mylib)
lua 5.2: luaL_newlib(L, mylib) 等價於

luaL_checkversion(L);
lua_newlibtable(L, mylib);
lua_setfuncs(L, mylib, 0);

其中 luaL_checkversion 函數會導致在 lua 解釋器程序中無法加載正常 c 模塊,會出現 multiple Lua VMs detected 錯誤,這是打開了兩個 lua 環境造成的;所以如果要導出供純 lua 程序使用的 c 模塊,要把 luaL_checkversion 這條語句去掉,即不要直接使用 luaL_newlib 函數

下面用實例演示一下整個過程,創建一個 c 動態鏈接庫程序,這個鏈接庫註冊了一個 clib 模塊,然後分別在 lua 解釋器和 c 程序中使用這個模塊

新建一個動態鏈接庫項目

使用 visual studio 創建一個控制檯項目 clib,勾選 dll 項目選項和空項目選項。新建一個文件 lib.c

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

//定義私有C函數
static int sayHello(lua_State* L)
{
    printf("hello world!\n");
    lua_pushstring(L, "hello world!\n");
    return 1;
}

//定義私有函數數組
static const struct luaL_Reg myLibs[] = 
{
    {"sayHello",sayHello},
    {NULL,NULL}
};

//定義外部函數
__declspec(dllexport) int luaopen_clib(lua_State* L)
{
    //5.1
    //luaL_register(L, "myLibs", myLibs);   

    //5.2
    //luaL_newlib(L, myLibs);
    luaL_newlibtable(L, myLibs);
    luaL_setfuncs(L, myLibs, 0);

    return 1;
}

然後編譯項目就會生成一個 clib.dll,這個文件就可以直接在 lua 程序中加載使用了。但如果要在 c 項目中使用的話,還需要創建一個頭文件 lib.h,不然的話 c 項目在使用時會找不到導出的外部函數

#pragma once
#include "lua.h"
__declspec(dllexport) int luaopen_clib(lua_State* L);

在 lua 解釋器中使用 c 模塊

把上面導出的 clib.dll 庫文件放入到 lua 程序運行時的 cpath 中去,然後在 lua 代碼中直接使用 require 加載模塊名即可,加載後要使用一個變量來保存模塊生成的 table

local clib = require("clib")

print(clib.sayHello())

程序到這裏就可以運行測試一下了,但運行之後很可能會崩潰,主要有下面幾點方要注意:

1、註冊的模塊名和 dll 文件名要保持一致,因爲 require 函數是根據名字找到 dll 文件,再從 dll 文件中加載同名的模塊

2、require 函數會自動在模塊內查找函數 luaopen_模塊名,然後運行這個函數完成註冊工作,因此模塊定義的外部函數名並不是可以隨便起的,而是必須使用 luaopen_模塊名,比如上面的 luaopen_clib,其註冊的模塊名就是 clib

3、__declspec(dllexport) 是必須的

4、 導出 c 模塊的時候需要用到 lua 鏈接庫,如果直接鏈接靜態鏈接庫,則生成的 c 模塊 dll 文件比較大,可直接加載;如果是鏈接靜態鏈接庫的 lib 文件,則生成的 c 模塊 dll 文件比較小,但不能直接加載,需要把 lua.dll 跟這個 dll 放在一起

在 c 程序中使用 c 模塊

我們也可以在 c 程序中打開一個 lua 環境,然後加載上面的 c 模塊到 lua 環境,最後執行 lua 代碼塊,讓 lua 代碼調用 c 函數。這種方法其實跟最上面講的 lua 調用 c 函數 一樣,不同的是前面都是一個函數一個函數手動註冊,而現在是通過一個外部模塊批量註冊所有函數。使用 lua_requiref 在 c 程序中加載 c 模塊完成函數註冊,函數原型如下
luaL_requiref(lua_State, "模塊名", 模塊外部接口, 1);

#include "lib.h"
#include "lualib.h"
#include "lauxlib.h"

int main()
{
    lua_State* L = luaL_newstate();

    //打開默認模塊
    luaL_openlibs(L);

    //打開自定義模塊
    luaL_requiref(L, "clib", luaopen_clib, 1); //1

    char buff[] = "clib.sayHello()";
    if (luaL_loadbuffer(L, buff, strlen(buff), 0))
    {
        printf("error load");
    }

    if (lua_pcall(L, 0, 0, 0))
    {
        printf("error run");
    }

    lua_close(L);

    getchar();
    return 0;
}
發佈了100 篇原創文章 · 獲贊 59 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章