M5Stack にて Lua を使う機会があり、ESP32 の環境で Lua を動かす方法についてまとめます。また、Lua の C/C++ バインドである Sol2 を使う方法についても触れます。
前提条件
- VSCode / PlatformIO 使用
- ビルドとアップロードを確認した環境
- Windows 10
- macOS Sequoia 15.3.2
- ビルドとアップロードを確認した環境
- Lua 5.4.7
- Sol2 3.3.0
- C++17 以降(Sol2を使う場合; 後述)
利用した ESP32 のハードウェアは M5Stack Basic (SoC: ESP32-D0WDQ6-V3) です。かなり昔の M5Stack ですが、他のハードウェアでも問題ないはずです。
準備: 環境構築
簡便のために、PlatformIO のプロジェクトを使ったコードを用意しました。
ビルド時に以下のディレクトリ構造になることを想定しています。*
がついたディレクトリとファイルはビルドまでに自動でダウンロードされます。
- include*
- sol*
- config.hpp*
- forward.hpp*
- sol.hpp*
- lib*
- lua* (Luaのリポジトリ または サブモジュール)
- src
- main.cpp
- patch.py
- platformio.ini
リポジトリのクローン
上記リポジトリを --recursive
オプションつきでクローンしてください。
$ git clone --recursive git@github.com:nanase/esp32-lua-example.git
# --recursive をつけずにクローンしてしまった場合
$ git clone git@github.com:nanase/esp32-lua-example.git
$ git submodule update --init --recursive
Sol2 の準備
Sol2 についてはリポジトリのクローン ではなく、Releases に添付されている 3つのヘッダファイルが必要です。
今回は後述する patch.py が自動でダウンロードを行うため、PlatformIO を利用する場合は手動での取得は不要です。
patch.py
ESP32 は long long
(64bit整数型)をサポートしていません。lua/luaconf.h 内には以下のマクロが存在しますが、
/*
@@ LUA_32BITS enables Lua with 32-bit integers and 32-bit floats.
*/
#define LUA_32BITS 0
このマクロを 1
へ変更せずにビルドを行うと次のエラーが発生します。
"Compiler does not support 'long long'. Use option '-DLUA_32BITS' or '-DLUA_C89_NUMBERS' (see file 'luaconf.h' for details)
Lua 5.4.7 現在、lua/luaconf.h ではこのマクロが必ず 0
に上書きされるような実装がされています。そのため、ビルドオプションに -DLUA_32BITS
を指定しても効果がありません。単純に lua/luaconf.h を直接編集すれば解決しますが、今回はコンパイル時に自動でマクロを書き換えるスクリプト patch.py を用意して対応しました。
さらに、今回はスタンドアロン版のLuaを使わないため lua/onelua.c と lua/lua.c をビルド対象外にするための処理を含んでいます。また、前述の Sol2 に必要なヘッダファイルのダウンロードも行います。
patch.py の実装は記事の主題から外れるため、以下にリンクだけを示しておきます。
platformio.ini
M5Stack Basic用の構成になっています。最低限必要なもののみ載せました。ここでのポイントは extra_scripts
, build_flags
, build_unflags
です。
extra_scripts
で patch.py をビルド開始時に呼び出して前述した lua/luaconf.h の書き換えなどを行います。
build_flags
, build_unflags
は Sol2 のために C++17 でビルドを行うための指定です。Sol2 は C++17 以降でしかビルドできないため、このフラグ指定がないとやはりビルドに失敗します。もちろん、Sol2 を使わず純粋な Lua 連携を行う場合は指定不要です。
[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
extra_scripts = pre:patch.py
build_flags =
-std=gnu++17
build_unflags =
-std=gnu++11
lib_deps =
m5stack/M5Stack@^0.4.6
PlatformIO の環境

サンプルコードに対応した環境を用意しています。ビルド・アップロードの際は env:example
から始まる環境をひとつ選択してください。Default を選択するとすべての環境についてビルドされます。
実装
Lua を直接使う場合
LuaからのC++関数の呼び出しと、C++からのLua関数の呼び出しを行う簡単なプログラムです。lua.h をインクルードする前に lua_writestring
, lua_writestringerror
, l_system
の3つの関数を定義する必要があります。
このプログラムは PlatformIO の環境に env:sample2-lua
を選択してください。
#include <Arduino.h>
#include <M5Stack.h>
#undef min
extern "C" {
int lua_writestring(char* s) {
printf(s);
return 0;
}
int lua_writestringerror(char* s) {
printf(s);
return 0;
}
int l_system(const char* cmd) {
return 0;
}
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
int l_add(lua_State* L) {
int a = lua_tointeger(L, 1);
int b = lua_tointeger(L, 2);
lua_pushinteger(L, a + b);
return 1;
}
int l_print(lua_State* L) {
const char* s = lua_tostring(L, 1);
Serial.println(s);
return 0;
}
void runLuaScript(lua_State* L, const char* script) {
if (luaL_dostring(L, script) != LUA_OK) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
}
void setup() {
M5.begin();
M5.Power.begin();
M5.Speaker.begin();
M5.Speaker.mute();
Serial.begin(115200);
lua_State* L = luaL_newstate();
lua_register(L, "add", l_add);
lua_register(L, "print", l_print);
runLuaScript(L, R"(
function mul(a, b)
return a * b
end
print("Hello Lua!")
)");
lua_getglobal(L, "mul");
lua_pushinteger(L, 4);
lua_pushinteger(L, 5);
if (lua_pcall(L, 2, 1, 0) == LUA_OK && lua_isinteger(L, -1)) {
printf("ret=%d\n", lua_tointeger(L, -1));
} else {
fprintf(stderr, "cannot exec mul. %s\n", lua_tostring(L, -1));
}
lua_pop(L, 1);
lua_close(L);
}
void loop() { }
Hello Lua!
ret=20
Sol2 を使う場合
Sol2を使いつつ同じ実行結果になるプログラムです。こちらはスタック操作が消え、かなり短いコードになっています。エラー処理は省略しています。
このプログラムは PlatformIO の環境に env:sample4-sol2
を選択してください。
#include <Arduino.h>
#include <M5Stack.h>
#undef min
#include "sol/sol.hpp"
void setup() {
M5.begin();
M5.Power.begin();
M5.Speaker.begin();
M5.Speaker.mute();
Serial.begin(115200);
sol::state lua;
lua["add"] = [](int a, int b) { return a + b; };
lua["print"] = [](const char* s) { Serial.println(s); };
lua.script(R"(
function mul(a, b)
return a * b
end
print("Hello Lua!")
)");
int ret = lua["mul"](4, 5);
printf("ret=%d\n", ret);
}
void loop() { }
バイナリサイズの変化
コードのシンプルさを鑑みると Sol2 のメリットが大きいのですが、ビルド結果の firmware.elf のサイズは増大します。ESP32 はフラッシュサイズが少なくとも数 MB 以上を持つ SoC が多いため、Lua 単体では大きな問題にはならないでしょう。しかし将来的に WiFi などを併用したいと考えると、バイナリサイズには余裕を持たせておきたいものです。
そこで今回は以下の条件でバイナリサイズを比較しました。番号は PlatformIO の環境とソースコードの番号に対応しています。
- M5Stackの初期化だけを行う場合(Luaは使わない)
- Luaだけを直接使う場合
- LuaとLuaの標準ライブラリを使う場合
- Sol2を使う場合
- Sol2とLuaの標準ライブラリを使う場合
3. と 5. にある Lua の標準ライブラリは以下のようにして読み込ませました。Sol2側も同じ標準ライブラリを選択しています。
lua_State* L = luaL_newstate();
luaL_openlibs(L);
sol::state lua;
lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::coroutine, sol::lib::table,
sol::lib::io, sol::lib::os, sol::lib::string, sol::lib::math,
sol::lib::utf8, sol::lib::debug);
結果は以下のようになりました。
条件 | Flashサイズ | 1. からの増分 | 標準ライブラリの増分 |
---|---|---|---|
1. | 385,641 バイト (376.6 KiB) |
n/a | n/a |
2. | 454,625 バイト (444.0 KiB) |
+ 68,984 バイト (67.4 KiB) |
n/a |
3. | 521,013 バイト (508.8 KiB) |
+ 135,372 バイト (132.2 KiB) |
+ 66,388 バイト (64.8 KiB) |
4. | 677,997 バイト (662.1 KiB) |
+ 292,356 バイト (285.5 KiB) |
n/a |
5. | 734,785 バイト (717.6 KiB) |
+ 349,144 バイト (341.0 KiB) |
+ 56,788 バイト (55.5 KiB) |
ビルドのホスト環境により数十バイトの変動はあるものの、概ね上記のようになりました。Lua 単体で 67.4 KiB の容量増に対し、Sol2 を使うとさらに 218.1 KiB の容量増となります。
Sol2 + Lua 標準ライブラリの構成では 717.6 KiB と、他の機能についても本格的に実装を始めると 1 MiB を超えるでしょう。
ESP32 の場合は フラッシュのパーティションを調整できます。すなわち platformio.ini の board_build.partitions
を指定すればプログラムに割り当てられる容量を変更できます。これによりプログラム実装後でもある程度の対応は可能かと思います。
参考文献(先駆者)
Lua 5.3.4 を使った方法について解説されています。サンプルコードはここで例示されているものがベースになっています。コードの解説なども大変参考になりました。
Lua 5.4.1 を使っています。スタンドアロン版Luaが不要な場合において参考にしました。
Lua 5.2.4 と Sol2 の連携方法について解説されています。基本的な Sol2 の使い方も紹介されており、参考にしました。