その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

IP Routing TableのGo実装 "go-iprtb" 書いた

GoのIP Routing Tableの実装について調べてみたところ、だいたいOSのRouting Tableを操作する系のライブラリがヒットし *1、そうではなくユーザー側コードでRouting Table相当の実装・処理をしたい時に使えそうなライブラリがパッと見当たらなかったのでそれを書いたという次第です。


さて、GoでシンプルなRouting Table実装を書くのは実に簡単で、以下のように書くだけでほぼ期待通りに動くと思います。

type RouteEntry struct {
	Destination net.IPNet
	Gateway     net.IP
	NwInterface string
	Metric      int
}

routes := map[string]RouteEntry{}
// ここでroutesにrouteを登録する

// `target net.IP` がrouting tableに含まれているかどうかをチェックする
var matched RouteEntry
var everMatched bool
for _, r := range routes {
	if r.Destination.Contains(target) {
		if !everMatched {
			matched = r
			everMatched = true
			continue
		}

		matchedMaskLen, _ := matched.Destination.Mask.Size()
		newRouteMaskLen, _ := r.Destination.Mask.Size()
		if newRouteMaskLen > matchedMaskLen { // for longest match
			matched = r
		}
	}
}

fmt.Println(matched)

この実装の問題としてはlongest matchを考慮するためにはRouting Tableのエントリをすべて線形に走査する必要があるため、Routing Table中のエントリ数が増えれば増えるほど計算量が増える、ということが挙げられます。
したがってTable中のroutes全てを1つのPrefix Treeに落としこんでやることで、対象のアドレスについてその木のパスを1回走査するだけで対象アドレスがRouting Tableにマッチしているかどうかを判断することができ、これにより計算量を減らすことができるという狙いがあります。


例えば 10.0.0.0/8, 192.0.0.0/8, そして 192.128.0.0/9 という3つのサブネットを考えたとき、それぞれの2進数表記は以下のようになります。表にはサブネットに属するゲートウェイについても記載しています。

Subnet Subnet Binary Gateway
10.0.0.0/8 00001010 00000000 00000000 00000000 GW1
192.0.0.0/8 11000000 00000000 00000000 00000000 GW2
192.128.0.0/9 11000000 10000000 00000000 00000000 GW3

太字はサブネットマスクを適用した後のアドレスを表わしています。このアドレスビット列についてTrie木を構築していくと以下のようになります。

                 R
                / \
              /     \
            0         1
           /           \
          0             1
         /             /
        0             0
       /             /
      0             0
       \           /
        1         0
       /         /
      0         0
       \       /
        1     0
       /     /
 GW1 [0]   [0] GW2
             \
             [1] GW3

† R: Root Node
†† [n]: Terminal Node

そして対象アドレスを2進数にしてこの木に対して最長マッチをさせていくと以下のような結果が得られます ([ ]で囲われた箇所は終端ノードを表わしています):

Target IP Target IP Binary Found Gateway
10.10.10.10 0000101[0] 00001010 00001010 00001010 GW1
192.10.10.10 1100000[0] 00001010 00001010 00001010 GW2
192.192.10.10 11000000 [1]1000000 00001010 00001010 GW3
127.0.0.1 01111111 00000000 00000000 00000001 N/A


今回書いたライブラリでは上記で挙げたような木構造による実装をしたところ、以下のような性能改善を実現できました。

$ go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: github.com/moznion/go-iprtb
cpu: 12th Gen Intel(R) Core(TM) i7-1280P
Benchmark_PrefixTreeAlgorithm-20        18535410                81.68 ns/op           64 B/op          1 allocs/op
Benchmark_LinearSearch-20                6148155               192.1 ns/op            96 B/op          1 allocs/op
PASS
ok      github.com/moznion/go-iprtb     2.968s

速い! 良かったですね。
なお他にもroute消去時の枝刈りなど細々としたパフォーマンスのチューニングが入っていたりもします。

ぜひご利用ください。

CのShared LibraryにしたGoのコードで簡単にダングリングポインタを発生させる

例えば以下のようなGoのコードを書き、

package main

import "C"
import (
	"fmt"
)

//export DoSomething
func DoSomething(cstr *C.char) {
	fmt.Printf("Go: %s\n", C.GoString(cstr))
}

func main() {
}

go build -buildmode c-shared -o libdosomething.so ./ としてCのShared Libraryを生成して、以下のようにCのプログラムに組み込みます。

#include <stdio.h>
#include <stdlib.h>
#include "libdosomething.h"

int main() {
    char *str = (char *)malloc(sizeof(char) * 4);
    str[0]='f';
    str[1]='o';
    str[2]='o';
    str[3]='\0';

    printf("C: %s\n", str);
    DoSomething(str);

    free(str);

    return 0;
}

そして gcc -L . main.c -ldosomething とこのCのコードをコンパイルし、LD_LIBRARY_PATH=. ./a.out のように実行すると

C: foo
Go: foo

という風に、もちろんこのコードは期待通りに動作します。

さて、Go側の DoSomething() の処理をgoroutineを使うように変更してみましょう。

package main

import "C"
import (
	"fmt"
	"time"
)

//export DoSomething
func DoSomething(cstr *C.char) {
	fmt.Println("Go: DoSomething is called")
	go func() {
		time.Sleep(3 * time.Second)
		fmt.Printf("Go: %s\n", C.GoString(cstr))
	}()
}

func main() {
}

そして説明のためにCのコードも以下のようにします。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "libdosomething.h"

int main() {
    char *str = (char *)malloc(sizeof(char) * 4);
    str[0]='f';
    str[1]='o';
    str[2]='o';
    str[3]='\0';

    printf("C: %s\n", str);
    DoSomething(str);

    free(str);

    printf("C: C string has been freed and it's sleeping...\n");

    sleep(5);

    return 0;
}

そして先程と同じようにCのプログラムをコンパイルして実行すると、

C: foo
Go: DoSomething is called
C: C string has been freed and it's sleeping...
Go: 3/_

という風に、めでたくGoの C.GoString(cstr) がダングリングポインタを参照したおかげでおかしな文字列が出力されました。

理由としてはおわかりのように単純で、goroutineの中でsleep (これは何らかの処理を模しています) している最中にGoの関数の引数として渡されたCの char *str がCの側で解放されてしまったため、いざsleepから目覚めてgoroutineの中で C.GoString(cstr) を実行するとダングリングポインタを参照してしまう、ということになります。なまじ変数をGoの世界に引き込んだので油断してしまい、Goの関数に渡された時点でGo世界にallocateされてcopyされているだろうとタカをくくっていたところそんなはずは無く、そりゃ *C.char なんだから明らかにそうなんですが、まあ普通にpointerでしたね......という感じです。

これを避けるためには以下のように同期的に (すなわちgoroutineの外で) Goの文字列にしてしまえば良いでしょう。goroutineの引数としてキャプチャしてしまっても良いと思います。

func DoSomething(cstr *C.char) {
	gostr := C.GoString(cstr)
	go func() {
		time.Sleep(3 * time.Second)
		fmt.Printf("Go %s\n", gostr)
	}()
}


こうして見ると原因は単純ですし、この例のようにsleepを使った露骨なコードだとわかりやすいのですが、実際のコードでこういう事が起こるとなかなかわかりにくいですね。ハマりました。疲れますね。

ts-dynamodb-attributes-transformer - TypeScriptオブジェクト向けDynamoDB Attributes変換器

TypeScriptのオブジェクトをAmazon DynamoDB Attributes (正確には aws-sdk-js-v3 がサポートする Record<string, AttributeValue>) に自動変換するTypeScript Transformer Pluginであるts-dynamodb-attributes-transformerをリリースしました。

npmからもご利用いただけます。

これはTypeScript Compiler APIを使ったCode Transformer (コード変換器) です。ざっくり言うと、コンパイル時にそのASTを見つつそこでコード変換を挿し込めるという面白いやつです。特定の言語におけるマクロに似ている感じがしますね。

ts-dynamodb-attributes-transformerはどういうTransformerなのかというと、例えば以下のようなTypeScriptコードがあった時、

import { AttributeValue } from '@aws-sdk/client-dynamodb';
import { dynamodbRecord } from '@moznion/ts-dynamodb-attributes-transformer';

interface User {
  readonly id: number;
  readonly name: string;
  readonly tags: Map<string, string>;
}

const record: Record<keyof User, AttributeValue> = dynamodbRecord<User>({
  id: 12345,
  name: 'John Doe',
  tags: new Map<string, string>([
    ['foo', 'bar'],
    ['buz', 'qux'],
  ]),
});

このTransformerは dynamodbRecord<T>(obj: T) (つまりこの場合は dynamodbRecord<User>(...)) を検出して、型パラメーター T (今回は User) の定義を読み取り、そして引数に与えられたオブジェクトの内容に従って以下のようなDynamoDB Attributesのコードに変換します。

const record = function (arg) {
    return {
        id: {
            N: arg.id.toString()
        },
        name: {
            S: arg.name
        },
        tags: {
            M: function () {
                var m;
                m = {}
                for (const kv of arg.tags) {
                    m[kv[0]] = { S: kv[1] }
                }
                return m;
            }()
        }
    };
}({
    id: 12345,
    name: 'John Doe',
    tags: new Map([
        ['foo', 'bar'],
        ['buz', 'qux'],
    ]),
});

実挙動としては dynamodbRecord<T>(obj: T) の関数呼び出しをごっそりDynamoDB Attributes (の生成関数) に置き換えるという動きをします。

これはTypeScriptのTransformer Pluginのため、実際の環境で利用するためにはそのTransformerをTypeScriptコンパイル時にうまくこのTransformerをフックしてあげる必要があります。現時点では ttypescript *1なんかを利用するのが手っ取り早いと思います。詳しくは使い方をご参照ください。

TypeScript Compiler APIを使ったTransformerにした利点としては、対象オブジェクトをその定義に従って自動的にDynamoDB Attributesにできるというのはもちろんのこと、コンパイル時に静的にコード変換されて解決されるため「ランタイムで動的にリフレクションなりなんなりをして頑張ってDynamoDB Attributes Objectを組み立てる」ということをしなくても良くなりパフォーマンスの向上が狙えるというのがあります。この高パフォーマンスが狙いでした。良かったですね。*2

[追記]
ベンチマークを類似のライブラリであるkayomarz/dynamodb-data-typesに対して取ってみたところ以下のような結果となりました。

node version: v16.17.0
dynamodb-data-types marshalling x 3,475,450 ops/sec ±0.45% (96 runs sampled)
ts-dynamodb-attributes-transformer marshalling x 13,405,409 ops/sec ±0.43% (91 runs sampled)
Fastest is ts-dynamodb-attributes-transformer marshalling

おおよそ3.85倍の性能向上が認められます。
[追記ここまで]


TypeScript Compiler APIを使ってTransformerを書いたのははじめてだったので、何かおかしな点などありましたらご指摘いただけるとうれしいです。
TypeScript Compiler API Transformerについては、index.d.ts を手で書いてユーザーに提供し、ユーザーはそのd.tsの中で定義されている関数を使ってコードを書き、Transformerはその関数呼び出しに対してコード変換を実行することでユーザー目線では単なる関数呼び出しのように使える一方で裏側では関数のSignatureに則ったコードに変換されている (つまり、無論実際には違うのですがイメージとしては「関数呼び出しが予めコンパイル時に行なわれている」感じ)、というのは個人的に面白いなと思いました。

実際に現場で使いはじめておりますが便利な感じがしています。ぜひご利用ください。

*1:TypeScriptコンパイル時に良い感じにTransformer Pluginをコンパイルフェイズに挿し込んでくれる便利なTypeScriptカスタムコンパイラ

*2:なお似たようなアプローチでJSON.stringify()を5倍程度高速化した samchon/typescript-json というプロジェクトがありました

docker composeでワンショットタスクを実行する

TL;DR

docker compose up --abort-on-container-exit

[追記]

docker compose run --rm ${service_name} で良かった......
[追記ここまで]




例えばなんらかのテストを実行する時、テスト用にDB等のストレージコンポーネントを用意してそいつに対し読み書きすることでend to endのテストを模擬しているというようなことがあると思います。
かつてはテストケースごとにデータベースプロセスを上げそれに対してread/writeを実行、ということもよくしていましたが *1、最近ではテスト起動時にストレージコンテナを立ち上げてそれを読み書きしている事も多くなってきました。


さて、そのような時に「テスト起動するにあたってはコンポーネントAとコンポーネントBを事前にマシン上で立ち上げておいてください」みたいなのはいかにも面倒くさいので、こういう時に便利なツールとしてパッと思い浮ぶのはdocker composeでしょう。
docker compose自体については本記事では説明しませんが、本記事ではそういった「ストレージのような永続的なライフサイクルを持つコンテナ」と「テストランナーのような一時的なライフサイクルのコンテナ」を同時に起動させ、テストランナーの実行が終了したらすべて店仕舞いさせるというワンショットタスクをdocker composeで実行する方法について記します。

version: '3.9'

services:
  redis:
    image: redis:7.0.4
    container_name: redis
    ports:
      - "6379:6379"
    network_mode: host
  app-test:
    image: app-test-env:latest
    container_name: app-test
    command: npm test
    volumes:
      - ./:/app
    working_dir: /app
    network_mode: host
    depends_on:
      - redis

上記の docker-compose.yml では redis というRedisコンテナと、app-testというテストランナーコンテナを同時に実行するという定義をしています。


まずapp-test はテストの実行にあたってRedisを利用するので depends_onredis を指定することでredisコンテナの起動を先に行うようにします。
なお、これはあくまでredisコンテナの起動を先に行うというだけで「Redisが6379番ポートで実際にlistenしてくれるまで待つ」ということをしてくれるわけではありません。なのでドキュメントにも「実際に使えるようになるまで待つ必要がある場合は適切な方法でやるように」と書かれていますので注意しましょう *2: https://docs.docker.com/compose/startup-order/

ネットワークの設定は適宜書き換えてください。この例では簡単のためにredisコンテナの6379ポートをホストの6379ポートに公開し、redisとapp-testの両方のnetwork_modeをhostモードにすることでホストネットワークを介して6379ポートを通じて通信できるようにしてあります。


と、ひとまずこのようにしておくと、docker compose up を実行することでテストの実行は可能となります。
しかし、テストランナーの実行が終了してもredisコンテナの実行は継続するため明示的にSIGINT等を送ってあげないとこのdocker compose upは終了しなくなります。healthcheckなどを適切に設定するとこのへん上手くやれるはずですが、ワンショットのタスクにそこまでやるのも凝りすぎでは?


ということで、docker compose up --abort-on-container-exit というふうに --abort-on-container-exit を付与して実行すると、いずれかのコンテナがexitした時にそのexit codeを引き継いでdocker composeを終了してくれるようになります。

--abort-on-container-exit   Stops all containers if any container was stopped. Incompatible with -d

[追記]
冒頭にも書いたように、こうしておいて docker compose run --rm app-test と実行すると良いです。
[追記ここまで]

良かった良かった、これで簡単にワンショットのタスクをdocker composeで実行できましたね。
ちなみに、exitしたら全体をabortさせたいコンテナを PID=1 にすればexitした際にすべてを終わらせてくれるのではないか、と思って app-test に対し init: true を指定してみましたがこれは効きませんでした。

小ネタ

docker compose runの時はそのserviceのログしか出てこないので問題無いので大丈夫なんですが (つまり何も問題が無い)、docker compose upの場合はすべてのサービスのログが流れてきます。その際、今回のようなケースだとRedisのログが見れてもあまりうれしいことは無いので抑制したい。というわけで

...
  redis:
    image: redis:7.0.4
    logging:
      driver: none
...

このようにlogging driverを none に設定するのですがこれは期待通りに動きません。

詳しくはこのissueに書かれているのですが、docker compose upのログは実行中のコンテナに実際にattachしたものが表示されるのであって、logging driverの設定とは別とのことです。
つまり、ここでredisコンテナに対してnoneを設定すると、docker compose upのログには表示される一方、logging driverを使っているロガー、例えば docker logs ${container_id} 等には表示されなくなるという挙動をするようです。

なので、起動オプション --attach でログを見たいコンテナを指定すると、docker compose upのログにはそれだけが表示されるようになります。今回の例だと --attach app-test などとすると良いでしょう。

特定のユーザーのイベントによるGitHub ActionsのActionを保留状態にしておき、後で手動実行できるようにする

dependabotだとかrenovateだとかを使ってライブラリのバージョンアップのpull requestを自動的に送ってもらう、というような機構を利用されている方が多いと思います。
常にこれらのpull requestに目を光らせておいて常に取り込み続けるというのが理想的な形・そうあるべきだとは思うのですが、ふと気を抜くとバージョンアップのpull requestが溜まっていき、pull request自身も改訂に改訂を重ねている......みたいなことが起きがちではないでしょうか。

そういった折、誰も結果を見もしないCI (i.e. GitHub Actions) だけが回り続けているのを見て「このチェックは『ライブラリアップグレード業』をやる時に手動で回せばコンピューティングリソースの削減になるのでは?」と思い、それを試したという次第です。

この記事では例として、renovateからのpull requestに対して自動でActionを回す (つまり一般的な "CI" ) のではなくレビュアーの承認が降りた際に回すようにしてみます。

Environmentを作る

RepositoryにGitHubのEnvironmentを作ります。例えば、"renovate" という名前のEnvironmentを作ってみることとします。

"Required reviewers" にチェックを入れて、renovateからのpull requestに対するActionの実行を承認するユーザーあるいはチームを指定します。
この用途の場合は "Deployment branches" は "All Branches" で適当かと思いますが、都度環境に合わせると良いでしょう。

Action定義を書く

ここは普通にGitHub Actionsの定義を書きましょう。
重要な点は、普段 (つまりrenovate以外が) 実行するActionとrenovateが実行するActionをそれぞれ用意するということです。

普段実行するActionの定義については

...
jobs:
  your-action:
    if: ${{ github.actor != 'renovate[bot]' }}
...

と書いておき、renovateが実行するActionの定義には

jobs:
  your-action:
    if: ${{ github.actor == 'renovate[bot]' }}
    environment: renovate

と書いておくことで、renovate以外の時は普通に自動実行され、renovateの時は environment: renovate が有効になり承認が降りないとActionが実行されないこととなります。

このyaml中のifにどういった条件が書けるかについては以下のドキュメントを参照すると良いでしょう。

今回はactor (イベントの主体) の名前によって判断するという素朴な方法を採りましたが、その他のcontextを使うと色々ハイテクなことができそうです。例えばrenovateに限らず「botの時」はこういうふうにするとか。

あと、手元のyamlではrenovateが実行するActionのイベントに on push で指定しているのですが、これは on pull_request で十分なのかもしれない。

こんな感じになる

この場合 "e2e-test.yml" というのが普段走るActionのyaml、 "e2e-test-for-renovate.yml" がrenovateの時に走らせるActionのyamlとなっています。
これはrenovateによって作られたpull requestなのですが、「普段走るAction」がスキップされており「renovateの時に走らせるAction」がpending状態になっていることがわかると思います。

この時、 "This branch is waiting to be deployed" からメニューをポチポチ選んでゆくと

このような状況になるので、Review Deploymentsから承認することでActionがマニュアル実行されるという寸法です。あとは実行結果を見て、マージするかどうかを精査しましょう!

課題

task定義を複数用意するのダサくない・メンテ面倒じゃない?

それは本当にそう! 何か良い方法があると良いのですが......

[追記]

あと、composite actionを使えば良いのではないか、というアドバイスもありました: Creating a composite action - GitHub Docs

知見ありがとうございます。

[追記ここまで]


承認できるReviewersを6人までしか指定できないのは少ないし、何より権威主義的だ!!!

個人だけでなくチームを指定できるのでそこである程度カバーできるのではないでしょうか。

依存アップデートのpull requestのActionが自動で完了してないと、更にマージするモチベーション下がらない?

そうかも......なのでモチベーション高く常に依存アップデートのpull requestを見てるよ・マージしてるよ、という環境では不要だと思います。

一方、ある程度まとめて依存のアップデートをしているような環境だと都度都度Actionが回っている必要が無い、ということでリソースを節約できるのではないか、というのがこの仕組みの狙いです。

とはいえ、この方法を運用しはじめてから日が浅いので今後何か良い点・悪い点が見えてくる可能性があります。どうなることやら。

zigコードから生成したdynamic libraryおよびstatic libraryをCでコンパイルして使う

TL;DR

zig ccを使うと色々と簡単、特にstatic linkをする場合はハマりにくく楽なので、使える場合は使うと良さそうです。
zig cc自体、gccやclangのdrop-in replacementを目的として出来たCコンパイラなので多くの場合はそのままポンと乗せ替えができそうですが、環境によっては色々な事情もあるでしょうし本記事ではgccを使う方法についても記します。

前提

zig init-libで吐き出されるコード (src/main.zig) を使います。

const std = @import("std");
const testing = std.testing;

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}

なお、export modifierが付いていないとライブラリを経由してCから参照することができません。

One of the primary use cases for Zig is exporting a library with the C ABI for other programming languages to call into. The export keyword in front of functions, variables, and types causes them to be part of the library API:
https://ziglang.org/documentation/0.9.1/#Exporting-a-C-Library

Cのコード (main.c) は以下の通り。zigで実装しているadd関数をCコード中で利用します。

#include <stdio.h>
#include <stdint.h>

int32_t add(int32_t a, int32_t b);

int main() {
    printf("Added: %d\n", add(123, 321));
    return 0;
}

zigとCの型の対応については以下のドキュメントを参照すると良いでしょう。
Documentation - The Zig Programming Language

なお試した環境の各種バージョンは以下の通りです。

$ uname -srvmo
Linux 5.15.0-47-generic #51-Ubuntu SMP Thu Aug 11 07:51:15 UTC 2022 x86_64 GNU/Linux
$ zig version
0.9.1
$ gcc --version
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

これは至って普通です。

zigのコードをsoファイルのdynamic libraryにするには、以下のようにzig build-libを実行すると良いです。-targetに何を指定できるのかについてはzig targetsコマンドの結果を参照すると良いでしょう。色々とクロスビルドできて便利ですね。

$ zig build-lib -dynamic -target x86_64-linux-gnu src/main.zig

すると、libmain.soというファイルが出力されるので

$ gcc -L . main.c -lmain

このようにgccdynamic linkすると、

$ LD_LIBRARY_PATH=. ./a.out
Added: 444

このように実行することができます。

なおzig ccを使う場合は"gcc"の部分を"zig cc"に書き替えるだけで動きます:

$ zig cc -L . main.c -lmain

ちなみにzig ccを使ってコンパイルしたバイナリを実行する場合はLD_LIBRARY_PATHを指定する必要が無くなるのですが、gccでも同じことをしたい場合は-Wl,-rpath,.みたいな感じでオプションを指定してRPATHにライブラリの検索パスを追加してあげると良いでしょう *1

static linkする場合はいろいろとzig側にオプションが必要になってきます。

$ zig build-lib -static -fPIC -fcompiler-rt -target x86_64-linux-musl src/main.zig

このようにするとlibmain.aというstatic libraryが生成されます。

今回はstatic linkをするので、glibcの代わりにmuslを使います *2
-fcompiler-rtcompiler-rtのシンボル情報を出力に含めるためのオプションです。これが無いと、static linkをする際にzigランタイムの持つシンボルを解決できず、Cコンパイラコンパイルできなくなります。
そしてmuslを使う場合は-fPICオプションにより位置独立コードとしてライブラリを出力する必要があります *3

そしてgcc側では

$ gcc -L . -static main.c -lmain

としてコンパイルするとstatic linkされた実行形式が得られることとなります *4

なお、gcc (あるいは別のCコンパイラ) の代わりにzig ccを使う場合はbuild-libのオプションは簡略化することができます。

$ zig build-lib -static -target x86_64-linux-musl src/main.zig
$ zig cc -L . -static main.c -lmain
まとめ
  • zigのコードからCのdynamic libraryおよびstatic libraryを作成してCに組み込むことができた
    • Cで書くにはしんどいコードをzigで書いて組み込む、みたいなadaptiveな使い方が比較的簡単にできそう
  • zig ccは便利

*1:パスについては絶対パスで指定してあげたほうが良いような気はしますが

*2:glibcだと主にNSS周りでトラブルになるので。muslはglibcの互換でstatic linkをオフィシャルにサポートしている

*3:-fPIEでも良い

*4:メモ: なぜか `gcc -I . -L . -static -static-libgcc -Wl,-Bstatic main.c -lmain` などとしてやらないと動かない時があった。けれど本文に書いたように `gcc -L . main.c -lmain` で十分なはず。

令和最新! Linux Desktop環境上の tmux (3.3a) でOSのクリップボードとtmuxのコピペバッファを直結する

Linux Desktop上でterminalを使っている時にOSのクリップボードとtmuxのコピペバッファを直結させたいという話です。
つまり Prefix + [ 等でtmux上で範囲選択してEnterを叩いた時にその内容がOSのクリップボードに格納され、Prefix + ]でペーストする時はOSのクリップボードから引っぱってきて貼り付ける、という挙動にしたいのです。なんでそういう挙動にしたいかというと、そういう設定で長らくmacOS上で生活してきたので……

TL;DR

Wayland環境の場合、xselxclipを使った方法は上手く動きません。wl-clipboardで提供されるwl-copywl-pasteを使わないと駄目でした。

example:

set -s copy-command 'wl-copy'
bind ] run "tmux set-buffer \"$(wl-paste)\"; tmux paste-buffer"

とりあえずこれで動いているという状況。

以下メモ。

tmux 3.2以降では copy-command が使える

tmuxのGitHub RepositoryのWikiにはClipboardという名前のそのものズバリな聖典があり、基本的にはこれを読むとほとんど解決します。なのでこれを読みましょうという感じなのですが、

tmux 3.2 introduced an option called copy-command to set a command to pipe to for all key bindings. This is used when copy-pipe is called with no arguments which is now the default. If the option is empty, the copied text is not piped.

To pipe to xsel(1):

set -s copy-command 'xsel -i'

The more complex configuration in the next section also works with tmux 3.2 and later versions.

https://github.com/tmux/tmux/wiki/Clipboard#how-to-configure---tmux-32-and-later

とあるように、tmux 3.2以降ではcopy-commandという設定項目が導入されているのでこれを使うと手っとり早いです。あらゆるモードでのtmux上でのコピー時に、これで指定したコマンドがフックされて実行されます。つまり、OSのクリップボードにコピーした内容を叩き込むコマンドをここに指定すると良いということです。上記の例では xsel が使われていますが、Wayland環境だと上手く動かなかったのでwl-copyに変えてあげる必要があるでしょう。
古いバージョンのtmuxの場合はドキュメントに書かれているようにもうちょい複雑な設定を細々書かなきゃ駄目で面倒なので、copy-commandがあって良かったですね。

ただこれはあくまでコピーの設定なのでペーストは別途設定する必要があり、ペーストについては聖典にもあんま有効な情報は書かれていません。
なので素朴に

bind ] run "tmux set-buffer \"$(wl-paste)\"; tmux paste-buffer"

とすることで Prefix + ] 時に wl-paste を実行してOS側のクリップボードから内容を引っぱってきて、それをtmuxのpaste bufferにつっこむことで実現しているという感じです。X11環境であればxsel/xclipも使えるのではないでしょうか。

set-clipboard を使えば良いのでは?

set -g set-clipboard on のようにしてset-clipboardを有効にしてやると、

that on both makes tmux set the clipboard for the outside terminal, and allows applications inside tmux to set tmux's clipboard (adding a paste buffer).
https://github.com/tmux/tmux/wiki/Clipboard#changing-set-clipboard

とドキュメントにも書いてあるようにtmux外のクリップボードと連携し、paste bufferについても連携するようであります。
一見するとこれで良いじゃん! となるのですが、set-clipboardのHow it worksのセクションに書かれているようにOSC 52に対応しているターミナルエミュレータを使う必要があります。まあ使えば良いんですが、使っているターミナルがOSC 52に対応していなかったので今回は諦めることに。
というか、試しにOSC 52対応のKittyをインストールして set-clipboard をonにして使ってみたのですが、tmux上でのコピーはOSのクリップボードへ行く一方で、OSのクリップボードからtmuxのpaste bufferに入れられないという挙動になってしまっており、まあなんか厳しいなと思って撤退しました。このへんそこまで深く検証してないです。

まとめ
  • 結局、tmux上のコピーイベントとペーストイベントにフックして、OSのクリップボードと適切にやりとりするソフトウェアを呼び出してインターフェイスする方法が安定する
    • Waylandだとwl-clipboardが提供するツールを使う必要がある。X11だったらxselとかxclipとかは依然使えそう。
  • OSC 52対応端末だと set-clipboardをonにするともしかしたら一発で動くかもしれない (軽く試した感じ駄目だった)。