上記C言語でつくる「あたたたた」を参考にしました。ありがとうございます。
上記はmain関数でうごく実行ファイルになりますが、ライブラリ化してLuaJITから動かしてみようと思いました。何か制約があるのか、includeするものが多いとできないのか、調べてみました。生成AIでは、atatata関数という形にすることは可能です!includeするものが多くても問題ありません。
関数化したCコード
// atatata.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
void atatata(void) {
char hako[256] = "";
int running = 1;
srand((unsigned int)time(NULL));
while (running) {
int x = rand() % 2;
const char *ch = (x == 0) ? "あ" : "た";
printf("%s", ch);
fflush(stdout); // 即座に表示
strcat(hako, ch);
if (strstr(hako, "あたたたた") != NULL) {
printf("\nお前はもう死んでいる\n");
running = 0;
}
}
}
共有ライブラリとしてコンパイル
Windowsで、Qtに登録して、Mingw一式ダウンロード、インストール、ユーザ環境変数を設定しました。gccが入っていれば大丈夫です。
# Linux/macOS
gcc -shared -fPIC atatata.c -o libatatata.so
# Windows
gcc -shared atatata.c -o atatata.dll
atatata.dllできました。
Windowsで、LuaJITから呼び出す、
-- test_atatata.lua
local ffi = require("ffi")
-- 関数シグネチャを宣言
ffi.cdef[[
void atatata(void);
]]
-- ライブラリをロード
-- local lib = ffi.load("./libatatata") -- Linux/macOS
local lib = ffi.load("atatata") -- Windows
-- 関数を呼び出す
lib.atatata()
ターミナル実行
PS \Downloads\LuaJIT21> .\luajit.exe .\test_atatata.lua
縺ゅ≠縺ゅ◆縺溘≠縺溘≠縺溘◆縺溘◆
縺雁燕縺ッ繧ゅ≧豁サ繧薙〒縺・k
Downloads\LuaJIT21> [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding('utf-8')
Downloads\LuaJIT21> .\luajit.exe .\test_atatata.lua
たあああたたあああああたたあたたああああああたあたあああたああたたたた
お前はもう死んでいる
Downloads\LuaJIT21> .\luajit.exe .\test_atatata.lua
あああたああたあたあたたたああたあああたたああたたたた
お前はもう死んでいる
Downloads\LuaJIT21> .\luajit.exe .\test_atatata.lua
たたああたたたあああたたあたあたあたたああたたああああたあたたたああたあああたたあたあたたたあたあたあたあたたあああたたたた
お前はもう死んでいる
Downloads\LuaJIT21> .\luajit.exe .\test_atatata.lua
ああたたたあたたたあたたたあああああたああたあああたあああたたああたあたたたた
お前はもう死んでいる
できました。
includeに関する制約
✅ 問題ないケース(ほとんど)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>
// など、標準ライブラリは問題なし
これらはC標準ライブラリなので、どのシステムでも使えます。
⚠️ 注意が必要なケース
#include <windows.h> // Windows専用
#include <pthread.h> // UNIX系専用
#include <SDL2/SDL.h> // 外部ライブラリ(別途インストール必要)
- OS固有のヘッダ: 該当のOSでのみ動作
- 外部ライブラリ: コンパイル時にリンクが必要
# 外部ライブラリを使う場合
gcc -shared -fPIC atatata.c -o libatatata.so -lSDL2
教育現場での活用例
パターン1: シンプルな関数
// calc.c
int add(int a, int b) {
return a + b;
}
local ffi = require("ffi")
ffi.cdef[[ int add(int a, int b); ]]
local calc = ffi.load("./libcalc")
print(calc.add(3, 5)) -- 8
パターン2: 複数の関数を持つライブラリ
// gamelib.c
#include <stdlib.h>
#include <time.h>
void init_random(void) {
srand((unsigned int)time(NULL));
}
int random_range(int min, int max) {
return min + rand() % (max - min + 1);
}
local ffi = require("ffi")
ffi.cdef[[
void init_random(void);
int random_range(int min, int max);
]]
local game = ffi.load("./libgamelib")
game.init_random()
print(game.random_range(1, 100))
まとめ
制約のポイント:
-
main関数は不要 → 通常の関数として定義 - 標準ライブラリのincludeは自由 → 問題なし
- OS固有・外部ライブラリ → 環境に応じて対応が必要
- コンパイル時にリンクすれば → どんなライブラリでも使える
方法2: 純粋なLuaで書き直す(推奨)
LuaJITでこのCソースコードと同等の処理をコンパイルせずに実行することは可能です。LuaJITはFFI (Foreign Function Interface) を使ってC言語の関数を直接呼び出せますが、このプログラムの場合、純粋なLuaコードで書き直す方が簡潔です。
以下、2つのアプローチを示します。
-- atatatata.lua
math.randomseed(os.time())
local hako = ""
local running = true
while running do
-- 0 または 1 を生成
local x = math.random(0, 1)
-- あ or た を選ぶ
local ch = (x == 0) and "あ" or "た"
-- 出力
io.write(ch)
io.flush() -- バッファをフラッシュして即座に表示
-- hako に追記
hako = hako .. ch
-- 末尾が "あたたたた" か確認
if hako:find("あたたたた") then
print("\nお前はもう死んでいる")
running = false
end
end
方法3: LuaJIT FFIでCの関数を使う(学習用)
-- atatatata_ffi.lua
local ffi = require("ffi")
-- C標準ライブラリの関数を宣言
ffi.cdef[[
int rand(void);
void srand(unsigned int seed);
unsigned int time(void *);
]]
local C = ffi.C
-- 乱数初期化
C.srand(C.time(nil))
local hako = ""
local running = true
while running do
-- Cのrand()関数を呼び出し
local x = C.rand() % 2
local ch = (x == 0) and "あ" or "た"
io.write(ch)
io.flush()
hako = hako .. ch
if hako:find("あたたたた") then
print("\nお前はもう死んでいる")
running = false
end
end
実行方法
luajit atatatata.lua
教育的なポイント
-
FFIの使い道: 方法2ではC標準ライブラリの
rand()やtime()を直接呼び出していますが、実際にはLuaのmath.random()の方が簡潔です。FFIは既存のCライブラリ(グラフィックス、物理演算など)を使いたい時に威力を発揮します。 -
文字列処理: LuaはUTF-8を適切に扱えるので、
"あたたたた"のようなマルチバイト文字も問題なく処理できます。 -
パフォーマンス: このプログラムでは差は出ませんが、LuaJITは純粋なLuaコードでもJITコンパイルで高速に動作します。
追記 flush() と find()
上記のコードは、Lua標準のI/Oライブラリと文字列操作を使った、非常にシンプルで実用的な書き方。
「バイト数を計算して切り出す方法」と、
今回の「flush() と find() を使う方法」の違いについて、それぞれの役割と仕組みを解説します。
1. io.flush() の役割: 「バケツの水をすぐ流す」
io.write(ch) だけでは、画面に文字がすぐに表示されないことがあります。これを解決するのが io.flush() です。
-
バッファリング(溜め込み)の仕組み
コンピュータは効率を重視するため、io.writeで文字を受け取っても、すぐには画面に出さず、メモリ上の「バッファ(一時置き場)」に溜め込みます。「ある程度溜まったらまとめて出す」方が、処理回数が減って高速だからです。 -
flushがすること
io.flush()は、「まだ溜まってないけど、今ある分を全部すぐに画面に出して!」 と命令します。 -
このプログラムでの効果
これがないと、文字が「あ...(しばらく待つ)...たたた」のように、ある瞬間にドバっとまとめて表示されてしまいます。1文字ずつ「あ、た、た...」とリアルタイムに動いているように見せるための演出用の命令です。
2. hako:find(...) の役割: 「中身を検索する」
hako:find("パターン") は、文字列変数 hako の中身を先頭から末尾まで走査して、指定した文字の並びがあるかを探す関数です。
前回の「バイト数で切る方法 (sub)」との違い
| 特徴 |
hako:find("あたたたた") (今回の方法) |
string.sub(hako, -15) (前回の方法) |
|---|---|---|
| やっていること | 「検索(サーチ)」 | 「抽出(スライス)」 |
| 処理のイメージ | 箱の中の文章を最初から全部読んで、「あたたたた」という言葉が含まれているか探す。 | 箱の一番底(最後)から15バイト分だけを取り出して、それが「あたたたた」と一致するか比べる。 |
| バイト計算 |
不要。 |
文字が何バイトか気にせず、人間が見たままの文字を指定できる。 | 必要。
「あ」は3バイトだから、5文字なら バイト...と計算が必要。 |
| 処理速度 | 文字列が長くなると遅くなる。
100万文字溜まると、毎回100万文字分探すため重くなる。 | 常に一瞬で終わる。
文字列が何億文字あっても、「最後の15個」しか見ないため爆速。 |
どちらが良いの?
今回の「あたたたた」ゲームの場合:
- **書きやすさ優先なら
find()**
- 「3バイト×5文字=15」のような計算をしなくていいので、コードが直感的で読みやすいです。
- 「お前はもう死んでいる」と判定したい場合も、文字数を数えずに書き換えるだけで済みます。
- コンピュータの仕組みを教えるなら
sub()(バイト計算)
- 「なぜ15なのか?(UTF-8だから)」という深い理解につながります。
- また、「データが巨大になったとき、毎回全部探す
findは効率が悪い。最後だけ見るsubは賢い」という、アルゴリズムの計算量(速度) の話にも発展させられます。
まとめ
-
io.flush(): 「溜め込まずにすぐ表示せよ」という命令。アニメーション的な表示に必須。 -
hako:find(): 文字列全体からの「検索」。バイト計算不要で楽だが、データが増えると探すのが大変になる。 -
string.sub(): 末尾の「抽出」。バイト計算が必要だが、データが増えても処理速度が変わらない。
「最初はわかりやすい find で作り、あとで『もっと速い方法』としてバイト処理を教える」という流れも面白いかもしれません。
いろいろあります。ありがとうございます。