Go製のマルチプラットフォームなクリップボードライブラリを作った
はじめに
Go でマルチプラットフォームで動く CLI を作っていて、その過程でクリップボードにコピーしたり、ペーストできるようなライブラリがほしくなった。
マルチプラットフォームで動く Go のクリップボードライブラリを探してみると、ほとんど出てこなくて、たぶん以下のみ
https://github.com/atotto/clipboard
これも Windows は WindowsAPI を使って外部パッケージに依存せずに動作できるようになっているが、Linux と macOS については xclip や pbcopy などの外部パッケージのコマンドを叩くだけのラッパーになっている。
WSL の ubuntu で動かしてみたのだが動作せずに exit status 1
などと cmd.Exec の外部コマンドを実行したときのエラーのみが返ってきて、困っていた。Linux や macOS のクリップボードについても、外部パッケージを使うのではなく、Go のみで実装できると安定するな、と思い、作成方法を模索し始めた。
できたもの(失敗作)
現状は Linux/Unix 環境用で動かすことが実現できていない。
https://github.com/d-tsuji/clipboard
Windows
Windows には Clipboard を操作する API が用意されている。
https://docs.microsoft.com/ja-jp/windows/win32/dataxchg/using-the-clipboard
全体の流れはこれがわかりやすいように思える。
http://wisdom.sakura.ne.jp/system/winapi/win32/win90.html
- クリップボードにデータを転送する専用のメモリの確保
- クリップボードは OpenClipboard を用いて書き込む権利を取得
- EmptyClipboard でクリップボードを空にする
- CloseClipboard でクリップボードを閉じる
- SetClipboardData でクリップボードにデータを渡す
atotto/clipboard ではどうなっていたかというと以下のように各種のプロシージャを取得している。
user32 = syscall.MustLoadDLL("user32") openClipboard = user32.MustFindProc("OpenClipboard") closeClipboard = user32.MustFindProc("CloseClipboard") emptyClipboard = user32.MustFindProc("EmptyClipboard") getClipboardData = user32.MustFindProc("GetClipboardData") setClipboardData = user32.MustFindProc("SetClipboardData")
openClipboard を使ってクリップボードを開いている。雰囲気は分かるが細部はわからず。。Windows のデベロッパーサイトには C++ の例は記載されている。
HANDLE GetClipboardData( UINT uFormat );
しかし Go の例は記載されていないので、自前でシステムコールを扱う必要があるのだが、どのように調べるのだろうか。わからず。
atotto/clipboard での実装は以下のような感じ
func waitOpenClipboard() error { started := time.Now() limit := started.Add(time.Second) var r uintptr var err error for time.Now().Before(limit) { r, _, err = openClipboard.Call(0) if r != 0 { return nil } time.Sleep(time.Millisecond) } return err }
func readAll() (string, error) { err := waitOpenClipboard() if err != nil { return "", err } defer closeClipboard.Call() h, _, err := getClipboardData.Call(cfUnicodetext) if h == 0 { return "", err } l, _, err := globalLock.Call(h) if l == 0 { return "", err } text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) r, _, err := globalUnlock.Call(h) if r == 0 { return "", err } return text, nil }
Windows API のラッパーに関してはいろいろリポジトリが揃っているようだったので、それのうちの 1 つを用いることにした。
例えば以下
今回は上の lxn/walk を使っている。これのおかげで直接はシステムコールを扱わずともクリップボードを扱える
func get() (string, error) { c := walk.Clipboard() return c.Text() }
OSX
Windows と同様に Apple が提供しているクリップボード用の API が存在する
https://developer.apple.com/documentation/appkit/nspasteboard?language=objc
Swift か Objective-C 用のライブラリでクリップボードに対して Write, Read はできる。が当然だが Go には存在しない。少し調べたが、Go でのラッパーもほとんど見つからなかった。WindowsAPI のようにシステムコールを発行して、Pure Go で実装できるのであろうか。わからない。
Go の実験的なリポジトリとして一部の cocoa の実装があったが NSPasteboard の仕組みは提供されていなかった。
https://github.com/golang/exp/blob/master/shiny/driver/gldriver/cocoa.go
が、以下の不思議なサイト1に Objective-C を cgo でラップしている呼び出している面白い実装があった。
Go のランタイムの仕組みには cgo という仕組みが合って、C のコードを Go で呼び出すことができる。が、ビルド時に C コンパイラが必要になるため、ライブラリとして提供するのは筋が悪いように思えた。
gmp/clip では以下のようにして Objective-C のコードを Go で呼び出すことに成功している。
package ns /* #cgo CFLAGS: -x objective-c -fno-objc-arc #cgo LDFLAGS: -framework AppKit -framework Foundation #pragma clang diagnostic ignored "-Wformat-security" #import <Foundation/Foundation.h> #import <AppKit/NSPasteboard.h> void NSObject_inst_Release(void* o) { @autoreleasepool { [(NSObject*)o release]; } } // ... void* _Nullable NSPasteboard_inst_GetString(void* o) { NSString* _Nullable ret; @autoreleasepool { ret = [(NSPasteboard*)o stringForType:NSPasteboardTypeString]; if (ret != nil && ret != o) { [ret retain]; } } return ret; } */ import "C" import ( "unsafe" "runtime" ) // ...
実際 GitHub Actions で macOS の OS 上でのテストが通ったので、上記のコードは正常に動くだろう。手元に masOS のパソコンがないため実際には試しせていない。
しかし当然ながら cgo を用いているため、ライブラリとして組み込んだ場合に C コンパイラが要求される。README には記載しているがライブラリとしては筋が悪い気がする。macOS に詳しくないので何も言及できないが、 WindowsAPI のときと同様にシステムコールを自前で発行すれば、cgo を用いてなくても実装できるのではないか?
Linux
Windows や macOS の仕組みとは異なり、Linux のカーネルそのものにはクリップボードの仕組みは提供されておらず、X11 に依存する。また Wayland (私は初めて知ったが)というディスプレイサーバプロトコルもクリップボードを提供できるよう。
Linux だと xclip や xsel というツールがよく使われるようで、実際 atotto/clipboard でも xclip, xsel のコマンドを叩くようになっている。
いずれもは C 言語で書かれていて、コマンドラインでクリップボードのコピーとペーストを提供している。コードは 1000 行くらい。コードの読みやすさとしては xsel のほうが読みやすかった。
X11 では selection という概念があって、Selection を使ってウィンドウとデータを交換することができる。
クリップボードでは PRIMARY と CLIPBOARD と SECONDARY という selection を使うことができる
- 公式マニュアル: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.1
- 日本語訳: http://www.maroontress.com/ICCCM/ja-icccm.pdf
領域 | 説明 |
---|---|
PRIMARY | 引数を 1 つだけとるすべてのコマンドに対して使用され、セレクションのメカニズムを用いるクライアント間のコミュニケーションの主要な手段。PRIMARY でマウスのクリックでペーストできるような仕組みも実現している。 |
SECONDARY | PRIMARY セレクションが存在して、ユーザがそれを乱したくないときに、データを獲得する手段として用いられる。 |
CLIPBOARD | アンドペーストやコピーアンドペーストするデータを保持するのに用いる。最も馴染みがある領域。 |
というもの
いずれにしても X11 サーバに対してプロトコル通信する必要がある。cgo を使って既存の X クライアントの実装を移植するのも一つの手だが、大変である。既存の実装は 1000 行くらいでそんなに多くはないが、cgo 自体が大変そうで、メモリ管理などを C 同様に明示的に実装する必要があったり、その他いろいろありそう。最初の取っ掛かりとしては重すぎる。
調べていたら X クライアントの Go 実装が存在した。もともと Google のコードだったが、メンテされていないから移植したらしい。
https://github.com/BurntSushi/xgb
で、このライブラリを使った X のクリップボードの実装があった。リポジトリ自体は Go 製のテキストエディタを実装することを目的としている。
https://github.com/aarzilli/nucular
aarzilli/nucular の以下の実装を多少修正すればライブラリとしても使えるので、これを使うことにした。
未解決の問題
未解決の問題があって、X に値をセットしたときに、セットしたプログラムが終了してしまうと、セットした値はなくなってしまう。xclip や xsel ではどうしているかというと、プロセスを fork()
して、fork した子プロセスから値をセットするようになっている。Wayland 用のクリップボードライブラリである wl-clipboard の実装もみてみたが、やはり fork()
するようになっている。また、子プロセスは、selection へ他のプロセスから割り込まれた(Ctrl+C など)ときに SelectionClear のイベントを受け取って、プロセスが終了するようになっている。
- デーモン化
Go では基本的に fork() の仕組みは提供されていないが os.Args[0]
や裏技的にシステムコールを発行することはできる。
今回は sevlyar/go-daemon を用いてプロセスを生成した。単一のアプリケーションとして動作させる場合はうまく動作した。しかしライブラリとして組み込まれる場合は機能しなかった。これは os.Args[0]
で外部プロセスを起動する仕組みであるために、他のリポジトリに組み込まれることは想定しておらず、組み込まれた場合は機能しない。VividCortex/godaemon や syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
を実行する方法も試したが、結局思った動作は得られなかった。
- イベントでプロセス終了
SelectionClear のイベントを受け取ったタイミングでプロセスが終了する実装は簡単である。channel と goroutine を組み合わせて、SelectionClear のイベントを受け取ったタイミングで doneCh <- struct{}{}
などとし、待っている側のゴルーチンは <- doneCh
としておけばよい。
まとめ
気軽にやってみよう、と取り組んでみたが xlib や Windows API など普段の業務よりも低レベルなプログラミングが要求されて、力不足を感じた。ないものは作る精神なので、低レベルなアプリも実装できる技術力を身につけていきたい。
-
Go 製の GitHub 風のバージョン管理システムを用いている: ↩