Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ jobs:
# Run all tests (including MySQL / Redis integration tests)
# Must run from test/lua so relative Lua file paths resolve correctly
(cd $GITHUB_WORKSPACE/test/lua && $GITHUB_WORKSPACE/build/bin/unit_tests)
./bin/flua
# Lifecycle contract: init/update/fini call counts and order,
# flua.set_exit semantics, exit code. Entry points are fixed to
# package "main", so no flags are needed.
./bin/flua "$GITHUB_WORKSPACE/cmd/test.lua"

# 限定 release:matrix 有 debug/asan/cov,不限定的话一次 push 要跑四遍。
- name: Run benchmarks
Expand All @@ -194,8 +197,9 @@ jobs:
run: |
set -eux
cd build
echo "function main() return 0 end" > test.lua
./bin/flua --repeat=2 test.lua
# Fixed paradigm: package "main" with init/fini; --once runs just those
printf 'package "main"\nfunction init() return 0 end\nfunction fini() return 0 end\n' > test.lua
./bin/flua test.lua --once
cd "$GITHUB_WORKSPACE"
# Only this repo's sources. Uploading Boost/CPM/usr headers makes Codecov
# drop file-level reports because paths cannot be matched to GitHub.
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/build_with_macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ jobs:
ccache -s
# Run all tests (including MySQL / Redis integration tests)
(cd $GITHUB_WORKSPACE/test/lua && $GITHUB_WORKSPACE/build/bin/unit_tests)
./bin/flua
# Lifecycle contract: init/update/fini call counts and order,
# flua.set_exit semantics, exit code. Entry points are fixed to
# package "main", so no flags are needed.
./bin/flua "$GITHUB_WORKSPACE/cmd/test.lua"

- name: Run benchmarks
run: |
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/build_with_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ jobs:
run: |
set -eux
cd build/bin
./flua.exe
# Lifecycle contract: init/update/fini call counts and order,
# flua.set_exit semantics, exit code. Entry points are fixed to
# package "main", so no flags are needed.
./flua.exe "$GITHUB_WORKSPACE/cmd/test.lua"

- name: Run benchmarks
shell: msys2 {0}
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,38 @@ ctest --test-dir build -V
### CLI Tool `flua`

```bash
./build/bin/flua <script.lua> --entry=<func> --jit_type=<0|1|2> --repeat=<N>
./build/bin/flua <script.lua | dir/> [--once] [--jit_type=<0|1|2>]
```

- `--entry`: Entry function name (default `main`)
Entry points are fixed and always live in package `main` — there are no flags to rename
them, so every app looks the same:

```lua
package "main"

function init() ... end -- called once at startup
function update() ... end -- called every frame
function fini() ... end -- called once before exit
```

- `<script.lua | dir/>`: a single script, or a directory whose `.lua` files are all compiled (sorted by name)
- `--once`: run `init` + `fini` only, without the `update` loop (one-shot script / self-check)
- `--jit_type`: `0`=TCC, `1`=GCC, `2`=INTERP (bytecode VM)
- `--repeat`: Repeat call count (for performance measurement)
- `--debug`: Enable debug mode (default `false`; when `true`, outputs generated C source / richer diagnostics)

`update` is called back-to-back; a script that wants a fixed cadence paces itself, e.g.
`os.sleep(1000)` at the end of `update` (`os.sleep` takes milliseconds).

Graceful shutdown (both run `fini` first): return a non-zero code from `update`, or call
`flua.set_exit(code)` from Lua. Note that `os.exit` still exits immediately and skips
`fini`. Signals are not handled by flua -- installing a handler for SIGINT / SIGTERM is
the application's own decision.

Each lifecycle phase is a separate top-level Call. That matters because fakelua has no GC:
its frame arena is only reset when a top-level Call starts (reentrant count back to zero),
so a `while true` loop inside a single Lua Call would make the arena grow without bound.
Keep `update` to one frame of work (pump IO/timers, dispatch requests).

### Installation & CMake Integration

#### Install to System
Expand Down
27 changes: 24 additions & 3 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,14 +244,35 @@ ctest --test-dir build -V
### 命令行工具 `flua`

```bash
./build/bin/flua <script.lua> --entry=<func> --jit_type=<0|1|2> --repeat=<N>
./build/bin/flua <script.lua | dir/> [--once] [--jit_type=<0|1|2>]
```

- `--entry`:入口函数名(默认 `main`)
入口名固定为 package `main` 下的三个函数,不提供改名参数,所有应用写法一致:

```lua
package "main"

function init() ... end -- 启动时调用一次
function update() ... end -- 每帧调用一次
function fini() ... end -- 退出前调用一次
```

- `<script.lua | dir/>`:单个脚本,或一个目录(把目录下所有 `.lua` 按文件名排序编译)
- `--once`:只跑 `init` + `fini`,不进 `update` 循环(一次性脚本 / 自检)
- `--jit_type`:`0`=TCC,`1`=GCC,`2`=INTERP(字节码虚拟机)
- `--repeat`:重复调用次数(用于性能测量)
- `--debug`:是否启用调试模式(默认 `false`,若为 `true` 则输出生成的 C 源码 / 更详细诊断)

`update` 返回后宿主立刻再调一次;需要固定节奏的脚本自己在 `update` 末尾 `os.sleep(毫秒)`
(fakelua 的 `os.sleep` 单位是毫秒)。

优雅退出(两种都会先执行 `fini`):`update` 返回非 0,或 Lua 侧调 `flua.set_exit(code)`。
注意 `os.exit` 仍是立即退出、跳过 `fini`。信号不由 flua 处理——要不要接管
SIGINT / SIGTERM 是应用自己的决定。

三个阶段各自是一次独立的顶层 Call。这一点很关键:fakelua 没有 GC,帧 arena 只在顶层 Call
入口(reentrant 计数回到 0)重置,若把 `while true` 写在一个 Lua Call 里,arena 会无限增长。
`update` 里只做一帧的活(泵 IO/定时器、分发请求)。

### 安装与 CMake 集成

#### 安装到系统
Expand Down
213 changes: 199 additions & 14 deletions cmd/main.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,202 @@
// flua -- the FakeLua script host.
//
// Standard lifecycle: init once -> update repeatedly -> fini once before exit.
//
// Why every frame has to be its own top-level Call: fakelua has no GC, so the
// frame arena is only reset when a top-level Call starts (reentrant count back
// to zero). A `while true` loop inside one Lua Call keeps that arena alive
// forever and per-request tables/strings pile up without bound (memory leak).
// update therefore does one frame of work (pump IO/timers, dispatch requests);
// the loop and the frame boundary are driven from here.
//
// Pacing is up to the script: the host adds no sleep and calls update again as
// soon as it returns. A service that wants a fixed cadence calls os.sleep(ms)
// itself (fakelua's os.sleep takes milliseconds).
//
// Graceful exit -- both run fini first:
// - update() returns non-zero: exit with that code;
// - flua.set_exit(code) from Lua: flags the request, takes effect once the
// current update returns.
// os.exit stays a hard exit that skips fini; use the above when cleanup matters.
// Signals are deliberately not handled here: what SIGINT/SIGTERM should do is the
// application's decision, so install a handler if you need one.
//
// Fixed entry points, always package "main" -- there are no flags to rename
// them, so every app is written the same way:
// package "main"
// function init() ... end -- called once at startup
// function update() ... end -- called every frame
// function fini() ... end -- called once before exit
//
// Usage: flua <file.lua | dir/> [options]
// --once run init + fini only, without the update loop
// (one-shot script / self-check)
// --jit_type=n 0=tcc 1=gcc 2=interp (default 0)
// --debug enable debug mode
//
// Services whose logic is entirely in Lua can use flua's built-in modules (e.g.
// sqlite.open for lookup tables) and need no C++ host of their own.
#include "fakelua.h"
#include "gflags/gflags.h"

#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>

using namespace fakelua;

// 定义命令行参数
// Command-line flags
DEFINE_bool(debug, false, "enable debug mode");
DEFINE_string(entry, "main", "entry function name, entry must return code(int) and has no parameter");
DEFINE_int32(repeat, 1, "the repeat run main function times");
DEFINE_bool(once, false, "run init + fini only, without the update loop (one-shot script)");
DEFINE_int32(jit_type, 0, "jit type: 0=tcc, 1=gcc, 2=interp");

namespace {

// Fixed paradigm: the entry points are hard-coded to the three functions of
// package "main"; no renaming flags are accepted.
constexpr const char *kInitEntry = "main.init";
constexpr const char *kUpdateEntry = "main.update";
constexpr const char *kFiniEntry = "main.fini";

std::atomic<bool> g_exit_requested{false};
std::atomic<int> g_exit_code{0};
bool ExitRequested() {
return g_exit_requested.load(std::memory_order_relaxed);
}

// Lua-side exit hook: flua.set_exit([code]). It only flags the request and
// returns; the current update frame runs to completion.
void RegisterExitApi(State *s) {
RegisterNativeFunction(s, "flua.set_exit", 0, true, [](State *state, CVar *args, int n) -> CVar {
const int code = static_cast<int>(GetNativeIntArg(state, args, n, 0, 0));
g_exit_code.store(code, std::memory_order_relaxed);
g_exit_requested.store(true, std::memory_order_relaxed);
return NativeNil(state);
});
}

// The argument is either a single .lua file or a directory (every .lua inside
// is compiled, in filename order).
std::vector<std::string> CollectLuaFiles(const std::string &path) {
namespace fs = std::filesystem;
std::error_code ec;
if (fs::is_directory(path, ec)) {
std::vector<std::string> files;
for (const auto &entry: fs::directory_iterator(path, ec)) {
if (entry.is_regular_file() && entry.path().extension() == ".lua") {
files.push_back(entry.path().string());
}
}
// Cross-file references resolve at runtime, so the order does not
// change semantics; sorting is only for reproducible builds.
std::sort(files.begin(), files.end());
if (files.empty()) {
std::cerr << "flua: no .lua file under " << path << "\n";
}
return files;
}
return { path };
}

[[noreturn]] void ReportMissingEntry(const char *name) {
std::cerr << "flua: entry '" << name << "' not found.\n"
<< " flua always calls init / update / fini from package \"main\"; the script needs:\n"
<< " package \"main\"\n"
<< " function init() ... end\n"
<< " function update() ... end\n"
<< " function fini() ... end\n";
std::exit(1);
}

// Runs init -> update* -> fini and returns the process exit code. A failed init
// (non-zero return or exception) skips fini.
int RunLifecycle(State *s, JITType jit) {
int exit_code = 0;

// Fixed paradigm: all three entries are mandatory (except update under
// --once).
if (!HasEntryFunction(s, jit, kInitEntry)) {
ReportMissingEntry(kInitEntry);
}
const bool loop = !FLAGS_once;
if (loop && !HasEntryFunction(s, jit, kUpdateEntry)) {
ReportMissingEntry(kUpdateEntry);
}
if (!HasEntryFunction(s, jit, kFiniEntry)) {
ReportMissingEntry(kFiniEntry);
}

bool init_ok = false;
try {
int init_code = 0;
Call(s, jit, kInitEntry, init_code);
if (init_code != 0) {
exit_code = init_code;
std::cerr << "flua: init returned " << init_code << ", exit\n";
} else {
init_ok = true;
}
} catch (const std::exception &e) {
std::cerr << "flua: init failed: " << e.what() << "\n";
exit_code = 1;
}

if (init_ok && loop) {
while (!ExitRequested()) {
int update_code = 0;
try {
Call(s, jit, kUpdateEntry, update_code);
} catch (const std::exception &e) {
std::cerr << "flua: update failed: " << e.what() << "\n";
exit_code = 1;
break;
}
if (update_code != 0) {
exit_code = update_code;
break;
}
}
}

// init succeeded -> run fini: cleanup is needed on update errors and on
// requested exits alike.
if (init_ok) {
try {
int fini_code = 0;
Call(s, jit, kFiniEntry, fini_code);
} catch (const std::exception &e) {
std::cerr << "flua: fini failed: " << e.what() << "\n";
exit_code = 1;
}
}
return exit_code;
}

} // namespace

int main(int argc, char **argv) {
gflags::SetUsageMessage("usage: ./flua --help\n"
"\n");
gflags::SetUsageMessage("usage: ./flua <file.lua | dir/> [options]\n"
"\n"
"Fixed entry points, always package \"main\":\n"
" package \"main\"\n"
" function init() ... end called once at startup\n"
" function update() ... end called every frame\n"
" function fini() ... end called once before exit\n"
"\n"
"update is called back-to-back; the script paces itself if it needs\n"
"to (os.sleep takes milliseconds). --once runs init + fini only.\n"
"\n"
"Graceful exit, runs fini first:\n"
" return a non-zero code from update\n"
" call flua.set_exit(code) from Lua\n"
"\n"
"Signals are not handled by flua; install a handler in the application\n"
"if it needs to react to SIGINT/SIGTERM.\n");
gflags::SetVersionString("0.0.1");
gflags::ParseCommandLineFlags(&argc, (char ***) &argv, true);

Expand All @@ -24,20 +208,21 @@ int main(int argc, char **argv) {
const FakeluaStateGuard guard;
const auto s = guard.GetState();

// Lua 惯例:未捕获的运行时错误打印消息后以退出码 1 结束,而不是让 C++
// 异常穿透到 libc++abi 触发 SIGABRT(退出码 134,FakeLua Phase16 Bug 4)。
// Lua convention: an uncaught runtime error prints the message and exits
// with code 1, instead of letting the C++ exception escape into libc++abi
// and abort the process (exit code 134, FakeLua Phase16 Bug 4).
// No signal handling here on purpose: catching SIGINT/SIGTERM and deciding
// what to do with them is the application's business, not the host's.
try {
RegisterExitApi(s);

CompileConfig cfg;
cfg.debug_mode = FLAGS_debug;
CompileFile(s, argv[1], cfg);

int code = 0;
for (int i = 0; i < FLAGS_repeat; i++) {
Call(s, static_cast<JITType>(FLAGS_jit_type), FLAGS_entry, code);
for (const auto &file: CollectLuaFiles(argv[1])) {
CompileFile(s, file, cfg);
}
std::cout << "code: " << code << std::endl;

return code;
return RunLifecycle(s, static_cast<JITType>(FLAGS_jit_type));
} catch (const std::exception &e) {
std::cerr << "flua: " << e.what() << std::endl;
return 1;
Expand Down
Loading
Loading