うたカモ技術ブログ

Lua C

Luaプログラミング   C言語ソフトウェアからLuaを呼び出す方法

post:     update: 

この記事では、C言語ソフトウェアからLuaスクリプトとその関数を呼び出す方法について紹介します。

Luaは、C言語用にLuaスクリプトの呼び出しライブラリ(liblua.so)を提供しています。 C言語で作られたソフトウェアはこれを利用することで、Luaスクリプトの実行が可能になります。

この仕組みを上手く使ってソフトウェアのコア処理の一部をLuaスクリプト側に記述すれば、一度コンパイルして作ったC言語ソフトウェアの挙動 を再コンパイルせずに変更できます。つまり、Luaスクリプトを書き換えるだけで、ソフトウェアのカスタマイズ・アップデートが実現できます。

ひと昔前ではPlaystation3、4の一部のゲームにLuaスクリプトが使用されていました。 そのため、Luaスクリプトを改造することで本来ゲーム上で出来ないことを可能にするHack術がITニュースで取り上げられた時期もありました。

実際のゲームではC++言語などで作られたソフトウェアからLuaスクリプトを実行していたりしますが、今回はC言語からLuaスクリプトを呼び出したいと思います。

今回も例に漏れず、仕組みから説明したいと思いますのでよろしくお願いします。

#実行環境OS
ubuntu 22.04 LTS 64bit
#Lua
Ver 5.4.7

目次

  1. 前提条件:Luaライブラリのインストール
  2. Luaスクリプトを呼び出す仕組み(Luaステート)
  3. C言語からLuaスクリプトを呼び出す
  4. C言語からLua関数を呼び出して実行結果を取得する
  5. Luaスクリプト修正による動作変更
  6. おわりに

前提条件:Luaライブラリ(liblua.so)のインストール

C言語からLuaスクリプトを実行するには、予め開発環境にLuaライブラリ(liblua.so)をインストールする必要があります。 以下にインストール方法について掲載します。これはLua公式サイトのダウンロードページに掲載されているものです。 まだインストールしていない方はここで実施しましょう。

user:~$ curl -L -R -O https://www.lua.org/ftp/lua-5.4.7.tar.gz
user:~$ tar zxf lua-5.4.7.tar.gz
user:~$ cd lua-5.4.7
user:~$ make all test
user:~$ sudo make install

このLuaライブラリは次節で紹介するC言語バイナリ用のLua実行環境(Luaステート)を構築するための実装です。 ライブラリを適切に使用することで、指定したLuaスクリプトやその中の関数を明示して実行することが可能になります。

sudo make installで/usr/local/libに共有オブジェクトのliblua.soが格納されます。 これでコンパイル時にgcc -o test test.c -llua -lm -ldlとすることで、C言語ソースのtest.cliblua.so(-llua)をリンクすることができます。

※なお、5.4.4以降のどこかで数学系とダイナミックリンク系の共有オブジェクト(ライブラリ)のlibm.so(-lm)libdl.so(-ldl)もリンクしないといけなくなったようです。

それでは、次節から説明に入ります。

Luaスクリプトを呼び出す仕組み(Luaステート)

C言語からLuaスクリプトを呼び出す仕組みについて説明します。

C言語で作成されたソフトウェアはLuaスクリプトを実行するために、Luaステートと呼ばれるオブジェクトを自身が管理するメモリ内に 構築(①)します。そして、その中に標準ライブラリと実行対象のLuaスクリプトをロード(②、③)し、実行処理をトリガーすることでLuaスクリプト の呼び出し(④)を実現しています。

Luaステートとは、そのソフトウェアが構築する専用のLua実行環境です。

C言語ソフトウェアではLua実行環境を構築し、その中でLuaスクリプトの管理と一連の実行処理の制御をすることで Luaスクリプトの呼び出しを実現しています。

このLuaステート(Lua実行環境)の初期状態は大半のLuaスクリプトが使用する標準ライブラリもロードしていない空の状態です。 そのため、Luaステート構築後はLua標準ライブラリを導入し、Luaスクリプトの実行に必要な準備をします。 そして、実行対象のLuaスクリプトをロードすることでスクリプトの実行が可能な状態にします。

後はLuaスクリプトの実行をC言語のソフトウェア側でトリガー(④)することで、C言語ソフトウェアからLuaスクリプトを呼び出せます。

ここで、ソフトウェアとLuaスクリプト間でデータの受け渡しが必要であれば、Luaスタックと呼ばれる専用のスタック領域を介して データをやり取りすることもできます(これに関しては次節で紹介)。

以上が、C言語ソフトウェアからLuaを呼び出す仕組みについての簡単な説明です。

補足
この仕組みによるLuaスクリプトの実行は、Luaインタプリタの内部動作と同等です。コンソール画面からLuaというキーワードで呼び出せる LuaインタプリタはC言語で開発されたソフトウェアです。LuaスクリプトやLuaコードをコンソールを通して読み込み、処理を実行します。

次はLuaインタプリタの実行例です。起動した後にprint('Hello')を読み込ませて実行した例です。

kamo@kamo:~$ lua
Lua 5.4.4  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> print('Hello')
Hello

このとき、LuaインタプリタによるLuaスクリプトやコード実行はLuaステートを介して実施されます。

つまり、開発者が作成したC言語ソフトウェア上でLuaライブラリ(liblua.so)を使用するということは、 Luaインタプリタが実現するスクリプト実行と同等な処理をそのソフトウェアに実装したことを意味します。

スクリプトとは、作業手順が記載された原稿です。そのため、実行主体であるソフトウェアはそのスクリプトを動的に解釈して処理を実行します。このことから、 スクリプトを書き変えるだけでソフトウェアのカスタマイズ・アップデートが可能になります。

これは非常に魅力的なメリットですが、逆にデメリットにもなります。 そのソフトウェアを利用するユーザー側でLuaスクリプトを弄ることができれば、容易に改造ができてしまうわけですから。概要で説明したゲームの事例がそうです。

メリットとデメリットを踏まえた上で、Luaとの連携によってソフトウェアのカスタマイズ・アップデートの容易性が向上する点を理解してもらえればと思います。

C言語からLuaスクリプトを呼び出す

それでは、Luaスクリプトを呼び出す処理をtest.cに記述し、その実行バイナリのtestからcall_mod.lua(と間接的にmod.lua)を呼び出してみます。

[mod.lua]
名前を入力すると、「Hello + 名前!」の形式でコンソールに表示するgreet関数と、2つの数値を加算するadd関数を定義したモジュールです。

-- mod.lua
-- これは「Luaプログラミング   文法まとめ その2(モジュール・メタテーブル)」で紹介したサンプルスクリプトです。
local greet = function(name)
    local user_name = tostring(name) or "unname"
    print("Hello " .. user_name .. "!")
end

local add = function(x, y)
    local x_val = tonumber(x) or 0 --Luaは基本的に文字列を扱うので数値変換が必要です。
    local y_val = tonumber(y) or 0

    return x_val + y_val
end

--外部公開したい関数をテーブルメンバーに登録してreturnします。
return {
    greet = greet,
    add = add,
}

[call_mod.lua]
上記掲載のmod.luaをロードして、greet関数とadd関数を実行するスクリプトです。 必要な引数のユーザー入力を受け持ち、値の入力後に各関数を実行するだけの単純なスクリプトです。

--call_mod.lua
local mod = require('mod')

--挨拶をします。
print('[Greeting]')
io.write('What\'s your name? :')
local name = io.read()
mod.greet(name)

--入力値xとyの加算結果を表示します。
print('[Calculation(Add)]')
io.write('data x :')
local x = io.read()

io.write('data:y :')
local y = io.read()

local result = mod.add(x, y)
print(x .. ' + ' .. y .. ' = ' .. result)

[test.c]
上記掲載のcall_mod.luaを実行するC言語ソースコードです。前節で紹介したLuaステートの構築、 ライブラリとLuaスクリプトのロード、そして実行処理が記述されています。

//test.c
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int status, result;
    lua_State *L;
    L = luaL_newstate();    //Luaステートの構築
    luaL_openlibs(L);         //Lua標準ライブラリの導入(インポート)

    status = luaL_loadfile(L, "call_mod.lua");     //Luaスクリプトをロード(ただし、まだ実行はしない)

    if(status) {
        fprintf(stderr, "Couldn't load file: %s", lua_tostring(L, -1));
        return 1;
    }

    result = lua_pcall(L, 0, LUA_MULTRET, 0);   //Luaスクリプトの実行

    if(result) {
        fprintf(stderr, "Failed to run scripts: %s\n", lua_tostring(L, -1));
    }
    
    lua_close(L);   //Luaステートのクローズ
    
    return 0;
}

[コンパイル]
それではコンパイルして実行バイナリを作りましょう。

今回、私の環境ではホームディレクトリ直下にtest-luaというディレクトリを作り、そこに上記掲載のソースファイルを保存した上でtest.cをコンパイルしました。 このとき、Luaライブラリ(liblua.so)とリンクするために、-lluaを指定します。

kamo@kamo:~/test-lua$ ls
call_mod.lua mod.lua test.c
kamo@kamo:~/test-lua$ gcc -o test test.c -llua -lm -ldl

上記を見ても分かる通り、コンパイル対象のファイルはC言語ソースコードのtest.cのみです。ここでは実行バイナリの名前をtestとしました。

[実行結果]
実行結果は以下になります。C言語ソフトウェエアのtestからLuaスクリプトを呼び出せています。

kamo@kamo:~/test-lua$ ./test
What's your name? : utakamo
Hello utakamo!
data x : 10
data y : 20
x + y = 30

C言語からLua関数を呼び出して実行結果を取得する

前節では、単にC言語ソフトウェアのtestからLuaスクリプトを呼び出すだけでした。そこで、ここでは明示的なLuaスクリプトの関数呼び出しとその処理結果の受け渡しについて紹介します。

C言語ソフトウェアから明示的にLuaスクリプトの関数を呼び、その処理結果を得るには、Luaスタックと呼ばれるデータ領域を介して必要な情報を受け渡す必要があります。

LuaスタックはLIFO(後入れ後だし)ですので、関数呼び出しに必要な情報はPushとPopによってソフトウェアのメイン処理とLuaステート間でやり取りされます。

C言語ソフトウェアからLuaスクリプトの関数を呼び出す場合、はじめに対象の関数(①)とその引数情報(②)のそれぞれをLuaスタックにPushします。

これにより、前節で紹介したLuaステートは予めロードしたLuaスクリプトから関数定義(今回の例ではadd_func)を参照して指定の関数を実行する準備が整います。

あとは、Luaスクリプトの関数呼び出しをトリガー(③)することで実行が開始されます。

実行が完了すると、Luaスタックに積まれた関数と引数情報はPop(削除)されます(④)。そして、その関数が戻り値を返す場合は その値がPushされますので、C言語ソフトウェア側は関数の処理結果をLuaスタックを通して得ることができます(⑤)。

このようにして、C言語ソフトウェアとLuaスクリプト間のデータの受け渡しが実現できます。

それでは、前節のcall_mod.luaとtest.cを修正し、call_mod.luaの関数(greet_func,add_func,get_ip)をtest.cから呼び出してみたいと思います。

[mod.lua]
前節でも掲載したモジュールです。前節からの変更点は一切ありません。今回も使用するため掲載しました。

-- mod.lua
-- これは「Luaプログラミング   文法まとめ その2(モジュール・メタテーブル)」で紹介したサンプルスクリプトです。
local greet = function(name)
    local user_name = tostring(name) or "unname"
    print("Hello " .. user_name .. "!")
end

local add = function(x, y)
    local x_val = tonumber(x) or 0 --Luaは基本的に文字列を扱うので数値変換が必要です。
    local y_val = tonumber(y) or 0

    return x_val + y_val
end

--外部公開したい関数をテーブルメンバーに登録してreturnします。
return {
    greet = greet,
    add = add,
}

[call_mod.lua]
前節で掲載したcall_mod.luaを改造して、外部提供用の関数定義のまとまりにします。今回はサンプル的な趣旨のgreet_func、add_func関数の他に、もっと実益のある処理をするget_ip関数も追加しています。

--call_mod.lua
local mod = require('mod') -- 前節に掲載したmod.luaをそのままインポートします

-- Luaスクリプトから外部ソフトウェアの実行と結果を得る関数です。
-- get_ip関数で使用するために定義しました。この内容は次回記事で取り上げます。
local execute_cmd = function(cmd)
    local handle = io.popen(cmd)
    local result = handle:read("*a")
    handle:close()
    
    return result
end

-- test.cに提供する関数を定義しています。
-- [引数なしの関数]
-- 挨拶をします。
function greet_func()
    print('[Greeting]')
    io.write('What\'s your name? :')
    local name = io.read()
    mod.greet(name)
end

-- 入力値xとyの加算結果を表示します。
function add_func(x, y)
    return mod.add(x, y)
end

-- [引数あり]
-- 指定インタフェースに紐づいたIPv4アドレスを取得します。
function get_ip(interface)

    local is_grep = execute_cmd('which grep &')
    local is_awk = execute_cmd('which awk &')
    local is_cut = execute_cmd('which cut &')
    local is_ifconfig = execute_cmd('which ifconfig &')
    local ipv4_adress = 'none'

    -- 必要な依存ツールが存在するか確認(1個でもなければエラー終了)
    if not is_grep or not is_awk or not is_cut then
        print('[Error]:Lack of dependent tools(grep/awk/cut).')
        return 'error'
    end

    -- ifconfigコマンドでインタフェースに紐づくipv4アドレスを取得する
    if is_ifconfig ~= '' then
        ipv4_address = execute_cmd("ifconfig " .. interface .. " | grep 'inet ' | awk '{print $2}' | cut -d ':' -f 2 | tr -d '\\n'")
    end

    -- IPv4アドレスに一致していればアドレス文字列、不一致であればnil
    local check_result = string.match(ipv4_address, "^%d+%.%d+%.%d+%.%d+$")

    if not check_result then
        ipv4_address = 'none'
    end

    return ipv4_address
end

[test.c]
前節のtest.cでは、LuaスクリプトのロードにluaL_loadfileを使用していましたが、ここではluaL_dofileを使用します。 luaL_dofileは指定したLuaスクリプトを読み込んだ後に実行します。今回では、この処理によりcall_mod.luaの関数定義がLuaステートに適用されたことを意味します。

また、Luaスクリプト内の関数の実行はlua_callで可能です。Luaスクリプトの実行用関数のlua_pcallではないので、そこだけ注意しましょう。

//test.c
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main() {
    int status;
    int select = 0;

    lua_State *L;
    L = luaL_newstate();    //Luaステートの構築
    luaL_openlibs(L);         //Lua標準ライブラリの導入(インポート)

    status = luaL_dofile(L, "call_mod.lua");     //Luaスクリプトをロードして実行(関数定義の読み込み)

    if (status) {
        fprintf(stderr, "Couldn't load file: %s", lua_tostring(L, -1));
        return 1;
    }

    fprintf(stdout, "[1]:greet_func\n");
    fprintf(stdout, "[2]:add_func\n");
    fprintf(stdout, "[3]:get_ip\n");
    fprintf(stdout, "Select no :");
    scanf("%d", &select);
    getchar();

    int x = 0;
    int y = 0;
    int result = 0;
    char interface[256];
    const char *address = NULL;

    switch(select) {
        case 1:
            lua_getglobal(L, "greet_func");  //関数名:greet_funcをPush
            lua_call(L,0,0);  //引数の個数:0、戻り値の個数:0
            break;
        
        case 2:
            fprintf(stdout, "data x :");
            scanf("%d", &x);
            fprintf(stdout, "data y :");
            scanf("%d", &y);

            lua_getglobal(L, "add_func");  //関数名:add_funcをPush
            lua_pushnumber(L, x);  //引数xの数値をPush
            lua_pushnumber(L, y);  //引数yの数値をPush
            lua_call(L, 2, 1);  //引数の個数:2、戻り値の個数:1

            result = lua_tointeger(L, -1);    //add_funcの戻り値を取得
            fprintf(stdout, "%d + %d = %d\n", x, y, result);
            break;

        case 3:
            fprintf(stdout, "Interface(NIC) :");
            scanf("%s", interface);

            lua_getglobal(L, "get_ip");  //関数名:get_ipをPush
            lua_pushstring(L, interface); //引数interfaceの文字列をPush

            lua_call(L, 1, 1);  //引数の個数:1、戻り値の個数:1

            address = lua_tostring(L, -1);    //get_ipの戻り値を取得

            fprintf(stdout, "[interface:%s] IPv4 Address = %s\n", interface, address);

            break;

        default:
            fprintf(stdout, "Unknown select number.\n");
            break;
    }
    
    lua_close(L);   //Luaステートのクローズ
    
    return 0;
}

[コンパイル]
今回は前節で作ったtest-luaディレクトリのcall_mod.luaとtest.cに対して、それぞれを上記コードに書き換えた上でコンパイルしてみました。 ※mod.luaは変わりません。

kamo@kamo:~/test-lua$ ls
call_mod.lua mod.lua test.c
kamo@kamo:~/test-lua$ gcc -o test test.c -llua -lm -ldl

[実行結果]
ソフトウェアからLuaスクリプトの関数に引数を与えた上で直接呼出し、返却値が取得できることが分かります。

その1:greet_func関数を実行する
kamo@kamo:~/test-lua$ ./test
[1]:greet_func
[2]:add_func
[3]:get_ip
Select no :1
What's your name? : utakamo
Hello utakamo!
その2:add_func関数を実行する
kamo@kamo:~/test-lua$ ./test
[1]:greet_func
[2]:add_func
[3]:get_ip
Select no :2
data x :1
data y :2
1 + 2 = 3

今回追加したmod.luaのget_ip関数は、システムが使用しているNICと紐づいたIPv4アドレスを取得する処理です。 紐づいたIPv4アドレスがあればそれを表示し、無ければ「none」と表示します。

内部処理としては、Lua関数から外部ツール(Linuxコマンドなど)を 実行してその結果を分解することで取得しています。外部ツールの呼び出しとその結果の取得については次回記事で紹介します。

その3:get_ip関数を実行する
kamo@kamo:~/test-lua$ ./test
[1]:greet_func
[2]:add_func
[3]:get_ip
Select no :3
Interface(NIC) :wlo1
[interface:wlo1] IPv4 Address = 192.168.4.56

Luaスクリプト修正による動作変更

実装方法の理解と実行確認ができたところで、今度はmod.luaのgreet関数の出力内容を次のように変更してみます。

--mod.lua一部抜粋
local greet = function(name)
    local user_name = tostring(name) or "unname"
    print("Nice to meet you " .. user_name .. "!!") --「Hello <name>!」から「Nice to meet you <name>!!」に変更
end

変更後、再コンパイルをしないでtestを実行してみると、mod.luaのgreet関数の変更が反映されていることが分かります。

kamo@kamo:~/test-lua$ ./test
[1]:greet_func
[2]:add_func
[3]:get_ip
Select no :1
What's your name? : utakamo
Nice to meet you utakamo!!

これがC言語ソフトウェアとLuaスクリプトを連携させる目的です。実行バイナリを再コンパイルせずにソフトウェアのカスタマイズ・アップデートが可能です。

今回は表示部を変更しただけですが、コアとなる処理を変更すれば、それを利用するC言語ソフトウェアの影響は大きくなります。

おわりに

今回はC言語ソフトウェアからLuaスクリプトを呼び出す方法について紹介しました。

組み込み機器上で動作するアプリケーションでは、Luaスクリプトを使用する場面は多々あると思うので 趣味や仕事でのソフトウェア開発にお役立てください。

参考文献