Tana Gone
Tana Gone
5 min read

Categories

neovimのluaと仲良くなるためにluaスクリプトを実行してみた。

  • 次のコマンドでソース・ファイルの実行ができる

    :source %
    :luafile %
    :messages
    :lua vim.cmd('messages clear')
    

    source code

    -- a.lua
    vim.opt.helplang = { "ja", "en" }
    print(vim.opt)
    vim.print(vim.opt)
    vim.print(vim.opt.helplang:get())
    

    Output(luafile %; messages)

    "a.lua" 2L, 49B
    table: 0x0100c2ee48
    {
      <metatable> = {
        __index = <function 1>,
        __newindex = <function 2>
      }
    }
    { "ja", "en" }
    
  • vimオブジェクトを含めてすべてのオブジェクトを列挙する

    -- グローバル空間(_G)にあるものをすべて列挙する
    local keys = {}
    for name, _ in pairs(_G) do
    	table.insert(keys, name)
    end
    table.sort(keys)
      
    for _, name in ipairs(keys) do
    	local value_type = type(_G[name])
    	print(string.format("Global: %-15s [%s]", name, value_type))
    end
    

    output

    Global: LazyVim         [table]
    Global: MiniAi          [table]
    Global: MiniIcons       [table]
    Global: MiniPairs       [table]
    Global: Snacks          [table]
    Global: _               [number]
    Global: _G              [table]
    Global: _PlenaryLeafTable [table]
    Global: _VERSION        [string]
    Global: ___bufferline_private [table]
    Global: ___neotree_selector_click [function]
    Global: arg             [table]
    Global: assert          [function]
    Global: bit             [table]
    Global: collectgarbage  [function]
    Global: coroutine       [table]
    Global: debug           [table]
    Global: dofile          [function]
    Global: error           [function]
    Global: gcinfo          [function]
    Global: getfenv         [function]
    Global: getmetatable    [function]
    Global: io              [table]
    Global: ipairs          [function]
    Global: jit             [table]
    Global: load            [function]
    Global: loadfile        [function]
    Global: loadstring      [function]
    Global: lpeg            [table]
    Global: math            [table]
    Global: module          [function]
    Global: newproxy        [function]
    Global: next            [function]
    Global: noice_C         [userdata]
    Global: nvim_bufferline [function]
    Global: os              [table]
    Global: package         [table]
    Global: pairs           [function]
    Global: pcall           [function]
    Global: print           [function]
    Global: rawequal        [function]
    Global: rawget          [function]
    Global: rawset          [function]
    Global: require         [function]
    Global: select          [function]
    Global: setfenv         [function]
    Global: setmetatable    [function]
    Global: string          [table]
    Global: svim            [table]
    Global: table           [table]
    Global: tonumber        [function]
    Global: tostring        [function]
    Global: type            [function]
    Global: unpack          [function]
    Global: vim             [table]
    Global: xpcall          [function]
    

    Lua言語が標準で提供しているオブジェクト群です。vim を介さずに直接呼び出せます。 • string : 文字列操作(string.format や string.gsub など) • table : テーブル操作(table.insert や table.sort など) • math : 数学計算(math.sin や math.random など) • io : 外部ファイルの入出力操作 • os : 時間の取得や、OSコマンドの実行(os.time、os.execute) • package : モジュールの検索パス(先ほど使った package.path)の管理

    Neovimには、非同期処理や高速な構文解析のために最初から以下の強力なライブラリがグローバル展開されています。

    • uv (または vim.loop)

      • Neovimの非同期処理やイベントループを支える Libuv(Node.jsでも使われているライブラリ)のオブジェクトです。
      • ファイルの非同期読み込み、タイマー処理(〇秒後に実行)、外部プロセスの監視などを実行できます。
    • jit

      • Neovimが採用している超高速なLua実行エンジン LuaJIT(Just-In-Timeコンパイラ)を制御・確認するためのオブジェクトです。
      • jit.version で現在のJITバージョンを確認したり、最適化の状態を取得できます。

    特殊なシステム変数・環境 • _G (グローバルテーブル) • Luaのすべてのグローバル変数が格納されている大元のテーブルです。 • vim や string、あなたが自作したローカルでない変数も、すべて実体はこの _G というオブジェクトの中にあります(例:_G.vim と vim は全く同じものを指します)。 • _VERSION • 現在動作しているLuaのバージョン(Neovimであれば基本 “Lua 5.1”)を返す文字列オブジェクトです。

  • vim.optに含まれるすべてのオブジェクトを列挙する

    -- 1. Neovimが持つすべてのオプション情報を取得する
    local all_options = vim.api.nvim_get_all_options_info()
      
    -- 2. アルファベット順に並び替えるためにキー(名前)だけを抽出してソート
    local keys = {}
    for name, _ in pairs(all_options) do
        table.insert(keys, name)
    end
    table.sort(keys)
      
    -- 3. ソートした順に vim.opt からオブジェクトを取得して出力
    for _, name in ipairs(keys) do
        -- vim.opt.[設定名] の形でオブジェクトにアクセスする
        local opt_object = vim.opt[name]
          
        -- 現在の設定値を安全に取得する(:get() メソッドを使用)
        local success, current_value = pcall(function() return opt_object:get() end)
          
        if success then
            -- 1行で綺麗に出力する
            print(string.format("vim.opt.%-15s = %s", name, vim.inspect(current_value)))
        end
    end
    

    コードのポイント • vim.api.nvim_get_all_options_info(): Neovimが内部で持っている number や shiftwidth などの全オプションの名前とメタ情報をテーブルで返してくれます。 • vim.opt[name]: 文字列を使って動的にオブジェクトへアクセスしています。(vim.opt.number と vim.opt[“number”] は同じ意味です) • opt_object:get(): vim.opt.xxx は特殊なオブジェクト(メタテーブル)であるため、その設定値(true や数値、テーブルなど)を取り出すには :get() メソッドを呼ぶ必要があります。 • pcall(): 万が一、特定の環境で取得できないオプションがあってもエラーで処理が止まらないように安全弁をかけています。 実行すると、数百行に及ぶNeovimのすべての設定項目とその現在の値(true / false / 数値 / リスト等)がコンソールにずらりと表示されます。

  • vimに含まれるすべてのオブジェクトを列挙する方法グローバルな vim テーブル(vim.api や vim.fn などの親玉)に含まれる直下のモジュールやオブジェクト、関数をすべて列挙するには、通常のテーブルと同様に pairs(vim) を使ってループ処理ができます。

    Neovimの vim テーブルの直下は通常のLuaテーブル構造になっているため、vim.opt のような特殊な工夫をせずとも簡単に列挙できます。


すべてのオブジェクト(キーと型)を列挙するコード

以下のコードをファイルに貼り付けて source % で実行してください。

  -- 1. アルファベット順に並び替えるためにキー(名前)だけを抽出
  local keys = {}
  for name, _ in pairs(vim) do
      table.insert(keys, name)
  end
  table.sort(keys)
  
  -- 2. ソートした順にオブジェクトの名前と「型(Type)」を出力
  for _, name in ipairs(keys) do
      local value = vim[name]
      local value_type = type(value) -- table, function, string などの型を取得
      
      -- 1行で綺麗に出力する
      print(string.format("vim.%-15s [%s]", name, value_type))
  end
  vim.api           [table]     <-- Neovim独自の強力なAPI群
  vim.cmd           [function]  <-- Vimコマンドを実行する関数
  vim.env           [table]     <-- 環境変数にアクセスするオブジェクト
  vim.fn            [table]     <-- 従来のVimScript組み込み関数群
  vim.g             [table]     <-- グローバル変数 (g:)
  vim.inspect       [function]  <-- テーブルの中身を文字列化する関数
  vim.keymap        [table]     <-- キーマッピングを設定するオブジェクト
  vim.lsp           [table]     <-- 内蔵LSP(言語サーバー)関連
  vim.opt           [table]     <-- オプション設定オブジェクト
  ...

さらに深掘り:中身を完全にのぞき見したい場合

もし「名前の列挙」だけでなく、vim テーブルの全構造を丸ごと見たい場合は、Neovimに内蔵されている vim.inspect() を使うと一発でダンプできます。 [1, 2]

  -- vimテーブルのすべての階層・中身を完全にシリアライズして出力
  -- (非常に長大なテキストになるので注意してください)
  print(vim.inspect(vim))

特定のサブモジュール(例:vim.api の中にある全関数の一覧など)だけを調べたい場合は、上記の列挙コードの pairs(vim) の部分を pairs(vim.api) に書き換えることで同様に確認できます。

  • luaのPlayerオブジェクトを呼出す。ファイルの相対パス(%)から絶対パス(%:p)を取出し、さらにヘッダー部を除去(%:p:h)しオブジェクトが記述されたplayer.luaファイルが存在するパスをpackage.path変数へセットする。そうすれば、requireの引数にファイル名を記述してnewメソッドを呼び出せる。

    -- 現在開いているファイルのディレクトリを検索パスに追加する
    local current_dir = vim.fn.expand("%:p:h")
    package.path = current_dir .. "/?.lua;" .. package.path
      
    -- その後で require する
    local foo = require("player").new("Taro", 100)
    vim.print("foo.hp = " .. foo.hp) -- テーブルのアドレス(あるいは__tostringの結果)が表示されます
    print("foo = ", foo) -- テーブルのアドレス(あるいは__tostringの結果)が表示されます
    
  • luaのオブジェクト

    -- 1. クラス(設計図)となるテーブルを定義する
    local Player = {}
    Player.__index = Player -- 自分自身をインデックスに指定
      
    -- 2. コンストラクタ(インスタンス作成関数)を定義する
    function Player.new(name, hp) -- コンストラクタの名前は任意 e.g. setup
    	local self = setmetatable({}, Player) -- 空のテーブルにメタテーブルを設定
    	self.name = name
    	self.hp = hp
    	return self
    end
      
    -- 3. メソッド(メンバ関数)を追加する
    function Player:attack() -- .attack(self)でもOK
    	print(self.name .. " の攻撃!")
    end
      
    -- ここを追加!print() されたときの表示形式を定義する
    function Player:__tostring()
    	return string.format("Player(Name: %s, HP: %d)", self.name, self.hp)
    end
      
    -- 4. オブジェクト(インスタンス)を作成して使ってみる
    -- local player1 = Player.new("勇者", 100)
    -- player1:attack() -- 出力: 勇者 の攻撃!
    return Player
    

    ポイント

    • setmetatable(table, metatable): テーブルにメタテーブルを設定し、独自の振る舞いやプロパティの継承を持たせます。
    • __index: テーブルに存在しないキーにアクセスされたとき、参照する別のテーブルを指定します。これにより、クラス側で定義したメソッドを各インスタンスから共有できます。
    • コロン構文(obj:method()): 第一引数に自動的に self(インスタンス自身)を渡すためのLuaの糖衣構文(ショートカット)です。 [1, 2, 3, 4, 5]
  • SwiftからLuaコードを実行データの受け渡しを行うには、LuaのC API(またはそれをラップしたサードパーティ製ライブラリである「SwiftLua」など)を利用します。

    ここでは、最も標準的で外部ライブラリへの依存が少ない、LuaのCライブラリを直接Swiftから呼び出す方法(LuaC / Lua 5.1 API)を想定したコードをご案内します。Neovimが内蔵しているLuaJIT(Lua 5.1互換)とも非常に親和性が高い書き方です。


  • 配列arを受取り配列brを返すluaのコードを実行するSwiftのコード

    import Foundation// ※LuaのCヘッダー(lua.hなど)をインポート可能な状態にしている前提です// (Swiftプロジェクトのブリッジングヘッダーなどで import しておきます)let ar: [Float] = [0.0, 1.0, 2.0]var br: [Float] = []// 1. Luaステート(仮想マシン環境)を作成let L = luaL_newstate()luaL_openlibs(L)// --- SwiftからLuaへのデータ転送 (ar配列をLuaのテーブルにする) ---lua_createtable(L, Int32(ar.count), 0) // 空のテーブルをスタックに積むfor (index, value) in ar.enumerated() {    lua_pushnumber(L, Double(value))    // Luaの配列は1始まりなので index + 1    lua_rawseti(L, -2, Int32(index + 1))}// グローバル変数 "ar" としてLua環境に登録lua_setglobal(L, "ar")// 2. Luaコードの定義と実行(仕様2: 各要素を2倍にする処理)let luaScript = """br = {}for i = 1, #ar do    br[i] = ar[i] * 2end"""// Luaコードを読み込んで実行if luaL_dostring(L, luaScript) != 0 {    let errorMsg = String(cString: lua_tostring(L, -1))    print("Luaエラー: \(errorMsg)")} else {    // 3. Luaコードが生成した配列(テーブル "br")をSwiftのbr配列へ代入    lua_getglobal(L, "br") // グローバル変数 "br" をスタックに取得        if lua_istable(L, -1) {        // Luaのテーブル(配列)の長さを取得        let len = lua_objlen(L, -1)                 for i in 1...len {            lua_rawgeti(L, -1, Int32(i)) // br[i] を取得してスタックのトップに積む            let value = lua_tonumber(L, -1)            br.append(Float(value))            lua_pop(L, 1) // 読み終わった値をスタックからポップ        }    }}// 4. Luaステートのクローズ(メモリ解放)lua_close(L)// 結果の確認print("ar: \(ar)") // [0.0, 1.0, 2.0]print("br: \(br)") // [0.0, 2.0, 4.0]
    

    コードの解説

    1. luaL_newstate() Swiftの中で独立した小さなLua実行環境(ステート)を立ち上げます。
    2. lua_createtable と lua_rawseti Luaの内部は「スタック」と呼ばれる仕組みでデータをやり取りします。Swiftの配列 [0.0, 1.0, 2.0] を1つずつLuaの数値に変換しながら、Lua側の配列(テーブル)の中に ar[1] = 0.0, ar[2] = 1.0 のようにセットしています。
    3. luaL_dostring(L, luaScript) 指定したLuaコードの文字列をその場でコンパイルして実行します。Lua側ではグローバルに配置された ar テーブルを読み込み、要素数を表す #ar を使ってループを回し、2倍にした新しいテーブル br を作成しています。
    4. lua_objlen と lua_tonumber Lua側で作られた br テーブルの要素数を調べ、1番目から順にSwiftの Float 型へキャストしながら、br.append() でSwift側の配列へ戻しています。

この実装を実際に動かすには、Swiftのビルド環境(XcodeやSwift Package Manager)に **Luaのスタティックライブラリ(`liblua.a`)やヘッダーファイル** をリンクさせる必要があります。
  • Swift向けのLuaラッパーライブラリ
  1. 主なGitHubリポジトリ(オープンソース・ライブラリ)
- ***\*[SwiftyLua (GitHub)](https://github.com/SwiftyLua/SwiftyLua)\****

  - SwiftからLuaを扱うためのブリッジ・ラッパーライブラリです。`Package.swift` への依存関係の追加方法や、テストコード内での具体的なユースケースのサンプルが用意されています。 [[1](https://github.com/SwiftyLua/SwiftyLua), [2](https://github.com/SwiftyLua/SwiftyLua)]

  

- ***\*[LuaSwift (GitHub)](https://github.com/tomsci/LuaSwift)\****

  - Lua 5.4のC APIを薄くラップした、外部依存関係のないシンプルなSwiftパッケージです。メタテーブルのブリッジングや、エラーをSwiftの `throw` に変換する機能など、実用的な連携機能が備わっています。 [[1](https://github.com/tomsci/LuaSwift), [2](https://github.com/tomsci/LuaSwift)]

  

- ***\*[LuaSwift - Lightweight Wrapper (GitHub)](https://github.com/ChrisGVE/LuaSwift)\****

  - iOSやmacOSアプリケーションにLuaを埋め込むためのモダンなSwiftインターフェースです。App Storeのガイドラインに準拠できるよう、LuaのCソースコードが直接バンドルされています。
  - このライブラリの詳細なドキュメントは、パッケージ管理サイトの ***\*[LuaSwift Documentation (Swift Package Index)](https://swiftpackageindex.com/ChrisGVE/LuaSwift/v1.12.4/documentation/luaswift)\**** からも確認できます。 [[1](https://github.com/ChrisGVE/LuaSwift), [2](https://swiftpackageindex.com/ChrisGVE/LuaSwift/v1.12.4/documentation/luaswift)]

  1. 日本語の解説記事・実装サンプル • 【Swift】Lua言語環境をアプリに組み込む【Cygnus】 (Zenn)
[【Swift】Lua言語環境をアプリに組み込む【Cygnus】](https://zenn.dev/enchan1207/articles/ac580ec2734183)
• 日本の開発者(Enchan1207氏)による、Lua環境を丸ごとSwiftアプリケーションに組み込むための解説記事です。SwiftとLuaの親和性や、macOS向けアプリに実際にLuaのスクリプト環境を実装する流れがグラフィカルに解説されており、非常に参考になります。
• swift-lua-example (GitHubサンプル)
[shengdie/swift-lua-example: SwiftからLuaを呼び出すサンプル](https://github.com/shengdie/swift-lua-example)

• SwiftとC言語の仕組み(ブリッジングヘッダーなど)を使い、SwiftからLuaを呼び出す最小限の構成を示した日本語のサンプルプロジェクトです。ライブラリを挟まず、生に近い形で連携させる際の参考になります。
  • requireメソッドでPlayerオブジェクトを呼び出したように他の言語、例えばSwiftで作成されたStruct、Classを呼び出すSwiftで作成した Struct や Class をNeovim(Lua環境)から利用したい場合、主に2つのアプローチがあります。

アプローチ1:SwiftコードをC言語の形式(Cリンケージ)にしてライブラリ化する

LuaはC言語との親和性が非常に高いため、Swift側で「C言語のフリをした関数」を作り、それをLuaの package.loadlib などの機能を使って呼び出します。

  1. Swift側のコード (Player.swift)

Swiftのクラスや構造体を直接Luaに渡すことはできないため、C言語から呼べる関数(ラッパー)を経由させます。

  // Swiftのクラスpublic class SwiftPlayer {    var name: String    var hp: Int        init(name: String, hp: Int) {        self.name = name        self.hp = hp    }}// C言語向けにエクスポートする関数@_cdecl("create_swift_player")public func createSwiftPlayer(name: UnsafePointer<CChar>, hp: Int32) -> UnsafeMutableRawPointer {    let swiftName = String(cString: name)    let player = SwiftPlayer(name: swiftName, hp: Int(hp))        // Swiftオブジェクトの参照カウントを維持したままポインタとして返す    return UnsafeMutableRawPointer(Unmanaged.passRetained(player).toOpaque())}
  1. Lua(Neovim)側のコード

このようにしてコンパイルした動的ライブラリ(例: libswift_player.dylib)を、Lua側から低レイヤーAPI(package.loadlib)や、LuaJITの ffi モジュールを使って呼び出します。特にNeovimに内蔵されている ffi を使うと、require のように手軽に外部ライブラリを扱えます。

  local ffi = require("ffi")-- C言語の型定義をLuaに教えるffi.cdef[[    void* create_swift_player(const char* name, int hp);]]-- Swiftでビルドした動的ライブラリを読み込むlocal swift_lib = ffi.load("./libswift_player.dylib")-- Swiftのオブジェクト(ポインタ)を作成local swift_player_ptr = swift_lib.create_swift_player("Taro", 100)print(swift_player_ptr) -- 外部オブジェクトのメモリアドレスが表示される

アプローチ2:RPC(リモートプロシージャコール)通信を使う(Neovimでの現実的な方法)

もし目的が「SwiftでNeovimのプラグインや拡張機能を作りたい」ということであれば、ライブラリ化するよりも、Neovimの RPC(Msgpack-RPC) という仕組みを使うのが一般的かつ安全です。

Neovimは裏で外部プロセスと通信する強力な機能を備えています。

  1. Swift側で独立したプログラム(実行ファイル)を作ります。
  2. NeovimのLua側から vim.fn.jobstart({"./swift_program"}) を使ってそのプログラムを起動します。
  3. 標準入出力(JSONやMessagePack形式)を介して、LuaとSwiftの間でデータのやり取りやオブジェクトの操作要求を行います。

まとめ

  • require("player") のように完全にLuaオブジェクトと同じ感覚で直接インポートすることはできない。
  • ただし、SwiftコードをC言語互換(@_cdecl)でビルドすれば、Neovimの ffi モジュールを使ってLuaからSwiftの処理を呼び出せる。