ぐるっとぐりっど

日曜プログラマがいろいろ試してみたことを、後の自分のためにまとめておく場所

高可用性k8sクラスタをkubeadmで構築してみる

はじめに

最近コンテナに入学したので、おうちクラスタを作ってみることにした。PCのメモリ64Gまで増強したので仮想マシンばんばん建てる体力もあるし。

といいつつ、シングルマスタのクラスタは、昔はやったラズパイクラスタ作るときに体験済で、せっかくなのでマルチマスタクラスタを組んでみることにした。 苦労はしたけど、苦労したポイントが

  • ドキュメントを右往左往させられる
  • タイポしてた

が大半なので、ほぼドキュメントへのリンク集。

構成

同一WindowsマシンのHyper-V上の仮想マシンとして構築した

用途 ホスト名 OS CPU メモリ *1
マスターノード master01.mshome.net Fedora 30 2コア 2GiB
master02.mshome.net Fedora 30 2コア 2GiB
master03.mshome.net Fedora 30 2コア 2GiB
コンピュートノード node01.mshome.net Fedora 30 4コア 4GiB
node02.mshome.net Fedora 30 4コア 4GiB
node03.mshome.net Fedora 30 4コア 4GiB
ロードバランサ lb01.mshome.net Fedora 30 2コア 2GiB

どこの設定か調べてないけど、mshome.netにするとホストのWindows機からも、追加設定なしに名前解決できて便利だったので、FQDNはそのように設定した。

インストール

仮想マシンの作成

Hyper-V仮想マシンの複製が、(簡単には)できないので、めんどくさい。

docker-ceのインストール

基本的にはドキュメントの通り。fedoraではyumのかわりにdnfが採用されて、基本的にyumコマンドが使えるのだけど、yum-utils は使えないのでレポジトリ追加は、 yum-config-manager のかわりに、

$ sudo dnf config-manager --add-repo url

とする必要がある。

Yum-utils package has been deprecated, use dnf instead. See 'man yum2dnf' for more information.

adding repo from: https://download.docker.com/linux/centos/docker-ce.repo YumRepo Error: All mirror URLs are not using ftp, http[s] or file. Eg. http://dummy

みたいな出力なので、docker-ceのrepoファイルがおかしいのかと思ってたら、そうではなかった。

master/computeの構築

ドキュメントのとおり。後述の問題のとおり、weaveを使う場合は、構築自体は完了するが、その後問題がおきるので要注意。

Creating Highly Available clusters with kubeadm - Kubernetes

ロードバランサの設定

apiをmasterに分散するために、ロードバランサが必要になる。今回はhaproxyを使った。 /etc/haproxy/haproxy.cfgに適当に設定書いた。

frontend k8s
    bind *:6443
    mode tcp
    default_backend k8s

backend k8s
    balance     roundrobin
    mode        tcp
    option      tcp-check
    server master01 master01.mshome.net:6443 check
    server master02 master02.mshome.net:6443 check
    server master03 master03.mshome.net:6443 check

問題点

corednsが動かない

kubectl get podで見てみる、corednsだけが動いてない(きづかずkubernetes-dashboardもdeployしてから気付いた)

$ kubectl get all -n kube-system
NAME                                              READY   STATUS              RESTARTS   AGE
pod/coredns-5c98db65d4-phqkw                      0/1     ContainerCreating   0          71m
pod/coredns-5c98db65d4-tznld                      0/1     ContainerCreating   0          71m
pod/etcd-master01.mshome.net                      1/1     Running             0          70m
pod/etcd-master02.mshome.net                      1/1     Running             0          32m
pod/etcd-master03.mshome.net                      1/1     Running             0          33m
pod/kube-apiserver-master01.mshome.net            1/1     Running             0          70m
pod/kube-apiserver-master02.mshome.net            1/1     Running             0          32m
pod/kube-apiserver-master03.mshome.net            1/1     Running             0          33m
pod/kube-controller-manager-master01.mshome.net   1/1     Running             1          70m
pod/kube-controller-manager-master02.mshome.net   1/1     Running             0          32m
pod/kube-controller-manager-master03.mshome.net   1/1     Running             0          33m
pod/kube-proxy-kdfbj                              1/1     Running             1          54m
pod/kube-proxy-ndcdt                              1/1     Running             1          55m
pod/kube-proxy-qvpkr                              1/1     Running             1          54m
pod/kube-proxy-spwnw                              1/1     Running             0          33m
pod/kube-proxy-vhjtg                              1/1     Running             0          32m
pod/kube-proxy-vw4w5                              1/1     Running             0          71m
pod/kube-scheduler-master01.mshome.net            1/1     Running             1          70m
pod/kube-scheduler-master02.mshome.net            1/1     Running             0          32m
pod/kube-scheduler-master03.mshome.net            1/1     Running             0          33m
pod/kubernetes-dashboard-7d75c474bb-9rp8z         0/1     ContainerCreating   0          28m
pod/weave-net-5kf5b                               2/2     Running             2          54m
pod/weave-net-5rbgf                               2/2     Running             2          55m
pod/weave-net-69v4x                               2/2     Running             2          54m
pod/weave-net-bc4sb                               2/2     Running             0          32m
pod/weave-net-jql64                               2/2     Running             0          59m
pod/weave-net-k6d2h                               2/2     Running             0          33m

より詳しく見てみるとこんな感じ

$ kubectl describe po coredns-5c98db65d4-phqkw -n kube-system
Name:                 coredns-5c98db65d4-phqkw
Namespace:            kube-system
Priority:             2000000000
Priority Class Name:  system-cluster-critical
Node:                 master01.mshome.net/192.168.112.71
Start Time:           Sun, 23 Jun 2019 09:07:10 +0900
Labels:               k8s-app=kube-dns
                      pod-template-hash=5c98db65d4
Annotations:          <none>
Status:               Pending
IP:
Controlled By:        ReplicaSet/coredns-5c98db65d4
Containers:
  coredns:
    Container ID:
    Image:         k8s.gcr.io/coredns:1.3.1
    Image ID:
    Ports:         53/UDP, 53/TCP, 9153/TCP
    Host Ports:    0/UDP, 0/TCP, 0/TCP
    Args:
      -conf
      /etc/coredns/Corefile
    State:          Waiting
      Reason:       ContainerCreating
    Ready:          False
    Restart Count:  0
    Limits:
      memory:  170Mi
    Requests:
      cpu:        100m
      memory:     70Mi
    Liveness:     http-get http://:8080/health delay=60s timeout=5s period=10s #success=1 #failure=5
    Readiness:    http-get http://:8080/health delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:  <none>
    Mounts:
      /etc/coredns from config-volume (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from coredns-token-kwrq4 (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             False
  ContainersReady   False
  PodScheduled      True
Volumes:
  config-volume:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      coredns
    Optional:  false
  coredns-token-kwrq4:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  coredns-token-kwrq4
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  beta.kubernetes.io/os=linux
Tolerations:     CriticalAddonsOnly
                 node-role.kubernetes.io/master:NoSchedule
                 node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason                  Age                    From                          Message
  ----     ------                  ----                   ----                          -------
  Warning  FailedScheduling        60m (x11 over 74m)     default-scheduler             0/1 nodes are available: 1 node(s) had taints that the pod didn't tolerate.
  Normal   Scheduled               60m                    default-scheduler             Successfully assigned kube-system/coredns-5c98db65d4-phqkw to master01.mshome.net
  Warning  FailedCreatePodSandBox  60m                    kubelet, master01.mshome.net  Failed create pod sandbox: rpc error: code = Unknown desc = [failed to set up sandbox container "cfeba2e58c4cbca0af13ce92e804804077f336ca4f5f11bfb8aca05f1822164a" network for pod "coredns-5c98db65d4-phqkw": NetworkPlugin cni failed to set up pod "coredns-5c98db65d4-phqkw_kube-system" network: failed to find plugin "loopback" in path [/opt/cni/bin], failed to clean up sandbox container "cfeba2e58c4cbca0af13ce92e804804077f336ca4f5f11bfb8aca05f1822164a" network for pod "coredns-5c98db65d4-phqkw": NetworkPlugin cni failed to teardown pod "coredns-5c98db65d4-phqkw_kube-system" network: failed to find plugin "portmap" in path [/opt/cni/bin]]
  Normal   SandboxChanged          5m29s (x257 over 60m)  kubelet, master01.mshome.net  Pod sandbox changed, it will be killed and re-created.
  Normal   SandboxChanged          15s (x12 over 2m20s)   kubelet, master01.mshome.net  Pod sandbox changed, it will be killed and re-created.

failed to find plugin "portmap" in path [/opt/cni/bin]とあるので、そうなんでしょう、と調べてみると、今回使った(ドキュメントどおりにやってくと導入される)CNIは、weave netなんだけど、これはportmapが不要なので導入しても一緒には入らないので、必要なら自分で入れろ、ということのようだ。

https://www.weave.works/docs/net/latest/kubernetes/kube-addon/

バイナリダウンロードしてきて配置して無事起動した(正確には、loopbackも必要だった)

ダッシュボード

デフォルトではWebUIは存在せず、cliだけなので、ダッシュボードを入れた

https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/

NodePortかなんか使えば外から見れると思うけど、一旦,kubectl proxyして、sshポートフォワーディングで確認。

f:id:grugrut:20190623142246p:plain

とりあえず今日のところはここまで

*1:動的メモリの設定してるので参考程度。これ書きながら確認したらマスタに3GiB割り当てられてた。

:ensure tを:straight tにおきかえるところから、straight.elを使ってみよう。MELPAでもgithubでもどこのパッケージも取得できるよ

自分は use-package で、emacsのパッケージ管理および設定管理をしている。

use-packageは、遅延ロードとかを意識しなくてもよしなにやってくれるし、なにより :ensureキーワードを使うことで、MELPAからダウンロードも自動でやってくれるので、パッケージの取得と設定を一元管理することができるのがよい。

ただ、そんなuse-packageだけど、不満点がひとつあり、 package.el をラッピングしてるだけなので、当然のことながら package.el の対象のサイトである、elpa, melpaなどからしかパッケージを取得することができない。

なので、githubで管理されていて、melpaのレシピが作成されていないものとか、melpaにあるんだけどバグがあってフォーク版がgithubにあるとか、そういったものを管理するのがめんどくさい。 自分でダウンロードして、load-pathに配置して、という作業が必要になってしまうのだ。

かといって、そのためにcask使ってみるのもなあ。。。と思っていた。quelpaとかも使ってみたのだけどイマイチぱっとしなかったり。

そんななか、straight.elの存在を知って使ってみたのですが、これがけっこういいかんじ

github.com

何がいいって、use-packageのキーワードに追加してくれるので、何も考えなければ、これまで :ensure t と書いてたところを :straight t と一括置換するだけで、とりあえず乗り換えが完了するのです。

(use-package dash
  :straight t)

こんな感じで、あまり移行を意識しなくてすむのがよい。

melpaで管理されているもののフォーク版を使いたいものの例としては、key-chordがあります。

qiita.com

これも、 :straightキーワードをちょっと変えて取得先を指定するだけなので、非常に簡単に書けます。

(use-package key-chord
  :straight (:host github :repo "zk-phi/key-chord" :branch "master"))

正直、ここまで意識せずに移行できると思わなかったので、なぜ早く乗り換えなかったのか悔むばかり。以前からstraight聞いてたけど、readme.mdがちょっと読みにくくて、どこがキモかよくわからなかったのよね。。。まあ、だからこそこのエントリを書くにいたったのですが。

use-packageで最近知った機能3選

最近、.emacsの整理をしていて、use-packageがデフォルトでdiminishを付属させないことを知りました。

で、ついでにREADME.orgをあらためて読みかえしてたら、これまで知らなかった面白い機能がいくつかあることを知ったので紹介。
(package.elを使うと、emacs上でアップデートができてしまうので、破壊的な変更に気付かずに、いつのまにか挙動変わってる...ってことがしばしばあるのが厄介ですね)

ちなみに、use-packageの基本的な使いかたは、kai2nenobuさんの、この記事がお勧め。

qiita.com

imenuに対応させる

個人的に今回かなりのヒットだったのがこれ。

(setq use-package-enable-imenu-support t)
(require 'use-package)

とすると、emacslisp-modeの場合に、use-packageの各パッケージ設定がimenuに載ってきてくれる。
ポイントは、requireの前に書くこと。

f:id:grugrut:20190105202730p:plain
use-packageをimenuに対応

これまで、init.elから目的の設定を探すのに、isearchしてみたり、helm-swoopしてみたりと苦戦していたのだけど、一発でジャンプできるようになった。

use-packageの状況を確認する

(use-package-compute-statistics t)

しておくと、M-x use-package-reportで、各パッケージの読み込み状況(:configまで進んだとか、:initまでとか)がわかる。

f:id:grugrut:20190105203002p:plain
use-package-report

状況が変化した日時と時間がわかるのだけど、多分累計じゃないと思う。org-modeのloadがこんな短いわけがないので。

key-chordを設定する

key-chordは、emacsに同時押しを実現するパッケージです。
use-packageのマクロにこの設定を組み込むことができるらしい。

(use-package use-package-chords
  :ensure
  :config (key-chord-mode 1))

(use-package view
  :ensure
  :defer t
  :chords (("jk" . view-mode)))

こんな感じで、:chordsキーワードが使えるようになる。
ただ残念なのが、使えるのがオリジナルのkey-chordなので、暴発問題が残っている。
こちらの改良版の方が便利だと思うので使いどころかも(うまく上書きできればよさげだけど)

qiita.com

ちなみに、このマクロ拡張のための機能が提供されているようで、純正のもの以外にも、hydraを使えるようにするuse-package-hydraなんてのもあるみたい。

おわりに

README.orgを読むと、他にもいろいろ書いてある(:afterキーワードでandとかorとか使えるってことも知らなかった。使いどころは?だけども)ので、当然のことながら、原典をまずは読むべし。

Emacsで簡単にゲームが作れるgamegrid.el

この記事は、Emacsアドベントカレンダー 18日目の記事です。

qiita.com

ちょっとEmacsで、わけあって *1 ドット絵的なものを書きたく、いろいろ調べてるうちにgamegrid.elに出会いました。
gamegrid.elは、Emacs本体に同梱されている、まさにgridなgameを作るためのパッケージです。
gamegrid.elで調べると、いろいろ出てきますが、emacsに内蔵されているpongとかsnake、tetrisなんかに使われています。

そして、使いかたがよくわからず、ぐぐっても使ってみた的記事があっても使い方がよくわからなかった(みつからなかったけど、infoに該当セクションあったりするのかな?)ので、実際使われているものをソースコードリーディングしながら使ってみました。

ソースコードリーディングする上では、snakeゲームのソースコードがわかりやすかった。tetrisはちょっと複雑。
github.com

作ったもの

github.com

f:id:grugrut:20181217225251g:plain
作ったもの

よくある上から降ってくるキャラを避けるゲーム。キー入力周りのところが若干おかしい。

内容と解説

理解しきれてないところもあるので、間違いあればマサカリください。

どうやって描画するか

(defconst sample1-player 0)
(defconst sample1-block 1)
(defconst sample1-blank 7)
(defconst sample1-border 8)
(defconst sample1-space 9)

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L39-L43

gamegridの名のとおり、gridベースなので、x, y座標に対して、そのセルが何であるかを指定することになる。その指定のための定数定義。

(defvar sample1-player-options
  '(((glyph colorize)
     (t ?\040))
    ((color-x color-x)
     (mono-x grid-x)
     (color-tty color-tty))
    (((glyph color-x) [1 0 0])
     (color-tty "red"))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L83-L90

(defun sample1-display-options ()
  "."
  (let ((options (make-vector 256 nil)))
    (dotimes (c 256)
      (aset options c
            (cond ((= c sample1-blank)
                   sample1-blank-options)
                  ((= c sample1-player)
                   sample1-player-options)
                  ((= c sample1-block)
                   sample1-block-options)
                  ((= c sample1-border)
                   sample1-border-options)
                  ((= c sample1-space)
                   sample1-space-options)
                  (t '(nil nil nil)))))
    options))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L127-L143

先の定数あたりも使って、どのセルをどう描画するかを決めるところ。Xの場合は、 [1 0 0]のようにvectorで、r g bを0から1の範囲で設定する。
ターミナルの場合は、"red"のように、指定できる。ターミナルの場合になにが使えるのかは調べてない。
また、Xを使う場合は、画像をxpm形式で指定できるはず。未指定の場合は、デフォルトのブロック画像が使われる。

この辺を駆使するため、以下の初期化処理でいろいろ設定する。

初期化処理と毎ターンの描画

基本的にはメジャーモード書いて作る感じ。それ以外のgamegrid特有の内容だと以下な感じ

(defun sample1-start-game ()
  "."
  (interactive)
  (sample1-reset-game)
  ;;(use-local-map sample1-mode-map)
  (gamegrid-start-timer sample1-tick 'sample1-update-game))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L154-L159

gamegrid-start-timerで何ミリ秒間隔でどの関数をコールするかを設定できる。このタイマー間隔がFPS維持するような仕組になってるかどうかは未調査。
sample1-reset-gameの中では、 (gamegrid-kill-timer)でタイマーを念の為リセット + 以下のような感じで、gamegrid-init-bufferにより、gamegridで描画する範囲と初期値を指定している。

(defun sample1-init-buffer ()
  "."
  (gamegrid-init-buffer sample1-width (+ 2 sample1-height) sample1-space)
  (let ((buffer-read-only nil))
    (dotimes (y sample1-height)
      (dotimes (x sample1-width)
        (gamegrid-set-cell x y sample1-space)))))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L173

ついでに、gamegrid-set-cellを使うことで、指定したセルを別の内容に塗り替えることができる。
なので、updateの内容も、変更点を計算して、キャラが移動する場合は、前の時刻のセルを消して、新しい座標のセルを描画する、とかの処理をすることになる。

ゲームの終了

(defun sample1-stop-game ()
  "."
  (interactive)
  (gamegrid-kill-timer)
  (gamegrid-add-score "sample1-scores" sample1-total-cell))

https://github.com/grugrut/sandbox-gamegrid/blob/19418173db7c17a2899e8247c6166bb89e11bd27/sample1/sample1.el#L210-L214

gamegrid-add-scoreを使うことで、ハイスコアランキング(ただしローカル)を簡単に作れて便利

その他

グリフをブロック以外に自分が描画したものも使えるらしいので、試してみたい。

まとめ

なんか旨い使いかたないかなーと思うのですが、更新が激しいものを作るには適してない(結構チューニングが必要)ようなので、ピクロス的なゲームとか作るのには向いてそうです。

*1:挫折したけど途中まで自分も秋口ぐらいからNESエミュレータ作ろとせっせと書いてた。なので、gongoさんのアドベントカレンダーがとても楽しみです

カップラーメンの新発売情報をいつでもチェックできるようにした(by golang)

好きなカップラーメンは、ぶぶか油そばです。こんにちは。

カップラーメンって日々あたらしい商品が出てて、コンビニのカップラーメンコーナーをみて、新商品をチェックしているのですが、昔流行ったスクレイピングすりゃいいんじゃね? という考えに至りました。あとチェックするのは好きですが、食べるのは健康のために控えてます。

そんなわけで、使い慣れようと思っているgolangでちゃちゃっと作ってみました。

http://www.grugrut.net/tools/ramen.html

利用技術

golangスクレイピングするのには、goqueryを使ってます。CSSセレクタライクな構文でさくっととってこれるので手軽でした。

https://github.com/PuerkitoBio/goquery

ページの作成には、標準のhtml/templateを使ってます。普段templateはHttpResponseWriterにくわせるぐらいしかしていなかったのですが、まあ普通にFileWriterにくわせればファイル作成もできるだろうとやってみると、当然のようにできました。bootstrap使うとデザインセンスが皆無でもそれなりのものができて便利。

	file, err := os.Create("ramen.html")
	if err != nil {
		log.Fatalln(err)
	}

	tmpl := template.Must(template.ParseFiles("tmpl.html"))
	tmpl.Execute(file, recordList)

まあページこだわってないとはいえ、もうちょっとデザインは考えたいのと、コンビニの商品とかスナック菓子とか、チェックしたいものはたくさんあるので、汎用化をさせて作りやすくしたいものです。

2017年の振り返り

2年ぶり3回目の1年間の振り返りまとめ。
前回も2年ぶり2回目だったらしいので、隔年でまとめたくなるのかな?

なんか最近時間の流れがあっというまで、今年どんな年だったかすっかり忘れそうなので、やっぱりまとめたほうがよいよなと思う。完全に自分用のまとめです。

github

githubをちゃんと使うようになった。今さらかよ、という感じは自分でもするのだけど、githubが社内プロキシでブロックされてるようなアリエンティな会社なので、使い道がなかった。
ちゃんと使うといっても、まだまだ自分のVPSでgitサーバ立ててたのを、外部サービスに配置しただけ感は否めない。

とはいえ、プルリク送ってみて、マージされたりしてるので、第一歩としては十分だろう。年末だし自分を積極的に褒めていく。
github.com

また、7月ぐらいから毎日コミットするぞ運動を開始して、3か月ぐらい続けた。結構穴だらけなのは、外で飲んでて0時過ぎてしまったことに多く起因する。反省である。
f:id:grugrut:20171231094108p:plain

本当はもっとコミットしてたはずなんだけど、.emacs.dリポジトリをdotfilesリポジトリに移動して(それも単なるmvで)、.emacs.dリポジトリを消してしまったため、そこの情報が多分消失している。.emacs.dは、もともとdropbox管理だったり、ともすれば単なるファイルサーバ配置だったりしてたので、今でもカジュアルに履歴を消してしまう。

emacs

slackとかlingrのinit.el読書会とか、色々なコミュニティに参加した。オンラインだけじゃなくて、もくもく会などにも参加して、そこで知らなかった便利パッケージを教えてもらったりするなどした。LTもプライベートで初めてやって、お仕事の発表と違ってまた楽しかった。

パッケージも作ってMELPAに登録することもできたし。
github.com

学習のまとめ

これまでこっちのブログに適当なメモもがんばってまとめたものも一緒くたにしてたけど、なんかメモは独立させようかな、というのとhugoにも興味があったので、別ブログを作ってみた。ドメインあるのに有効活用これまでしてなかったというのもある。

http://www.grugrut.net/til/

これは以前書いたToday I Learnedをhugoでpublishしたものだ。
grugrut.hatenablog.jp
なので、githubリポジトリに同じ情報がある。org-modeで書いているので、とりあつかいもしやすい。

今後、sandboxリポジトリも作って、書き散らすコードはそっちでまとめて、うまく連携させれないか模索するつもり。

来年の抱負

去年までに比べれば、今年はわりといろいろと精力的に取り組んだほうだと思うけど、なんだかんだ仕事や飲みいくのを理由にさぼってたところがあるので、もっと形になるものを来年は取り組んでいくつもり。

その他

なんかここ1年で白髪がどばっと増えた。3年後ぐらいには半分ぐらい白くなってるんじゃなかろうか。

emacs25で追加されたdynamic moduleを使ってemacs上でパケットキャプチャしてみる

これは、 Emacs Advent Calendar 2017の4日目の記事です。

昨日は、j8takagiさんの連想リストのUPSERTでした。自分はどうせassoc使うと前の方から見て最初にequalだったものを取得するし、と、同じキーが入っても気にせずappendで更新したいキーをつっこんでます。更新型RDBMS的な。

dynamic moduleとは

去年の9月にリリースされたemacs 25.1に追加された機能で、リリース時のメールでは、リリースのハイライトの一番上に君臨しております*1

この辺の説明読んでて、これまで誤解してたのですが、この機能は「Emacsで、ネイティブの共有ライブラリが読めるよ」ということではなくて、「共有ライブラリに、emacsの関数を書いて、emacsでrequireできるよ」が正しいです。
任意のライブラリファイルが読めて、emacsの橋渡しというかラッパーは、emacs lispで書けばいいのかと勝手に思ってたのですが、共有ライブラリのところから自分で書いて、emacsから呼びだす関数やらもろもろ自分で作らないといけないようです。わかってから考えれば当然なのですが。

ちなみに、共有ライブラリは、やはりCで書くのが基本のようですが、調べてみるとgolangでもできるようです。というか本質的には共有ライブラリがコンパイルできる言語であればきっとなんでもよいはず。

ということで、思った以上に使うの面倒そうだったので、実際どんな感じで使うのか、試してみました。
普段はgolang書いてますが、golang使えることがわかったのが、Cで書いては消しを繰り返し、内容を自分で消化したあとだったので、今回は、Cで。

作ったもの

emacsでパケットキャプチャできるもの。先行の有名なソフトに敬意を示して、emacsharkと名付けてみた。今はすべてのIP通信をキャプチャして、送信元と送信先IPアドレスを表示するだけになってます。

f:id:grugrut:20171203130243g:plain

ソースコードは、githubにあります。

github.com

dynamic module用の書き方

既存のもの参考にしたり、ドキュメントを参照しながら書いたのだけど、結構クセがあるので、はまったところや感心したところなどをまとめておく

GPL互換ライセンスであることを明記する
int plugin_is_GPL_compatible;

https://github.com/grugrut/emacshark/blob/master/emacshark.c#L23

GPL互換であることを表明しておかなければならないらしい。 GPLって動的リンクするものまでは問わなかった気がするけど、どうせEmacsプラグインGPL互換ライセンス以外選択する気ないので、深くは考えてない

関数の定義
static emacs_value
Femacshark_init(emacs_env *env, ptrdiff_t nargs, emacs_value args[], void *data)
{

https://github.com/grugrut/emacshark/blob/master/emacshark.c#L51-L53

/* Bind NAME to FUN.  */
static void
bind_function (emacs_env *env, const char *name, emacs_value Sfun)
{
  /* Set the function cell of the symbol named NAME to SFUN using
     the 'fset' function.  */

  /* Convert the strings to symbols by interning them */
  emacs_value Qfset = env->intern (env, "fset");
  emacs_value Qsym = env->intern (env, name);

  /* Prepare the arguments array */
  emacs_value args[] = { Qsym, Sfun };

  /* Make the call (2 == nb of arguments) */
  env->funcall (env, Qfset, 2, args);
}

#define DEFUN(lsym, csym, amin, amax, doc, data)                        \
  bind_function (env, lsym,                                             \
                 env->make_function(env, amin, amax, csym, doc, data))

  DEFUN ("emacshark-init", Femacshark_init, 0, 0, "Init emacshark", NULL);

https://github.com/grugrut/emacshark/blob/master/emacshark.c#L129-L158

前半が処理を書くところ、後半がemacsで呼ぶための設定を書くところです。
前半のFemacshark_initの第二引数のnargsに相当するところがemacs側で呼んだ際の引数になり、例えば(some-func a b c)としたら、[a, b, c]が配列としてわたされてくる。
あとは、数値を返したければ

return env->intern(env, "nil");

すればいいし、数値や文字列を返したければ、

return env->make_integer (env, 42);
return env->make_string(env, str, len); // strがchar配列、lenが文字列長(\0のカウントは不要)

とすればよい。

後半で、ラッパー部分を定義していて、関数名や引数の最小数、最大数のほか、docstringなんかも書けるので、cでemacs lispを書いているような、そんな気分。

関数を定義する際は、戻り値がemacs_value型の関数を書いたうえで、さらに、いろいろとしないといけないので、結構面倒。
公式のサンプルソースで、なんか関数や関数マクロ駆使して、いいかんじに書く方法紹介されてたので、そのまんま真似したけど、emacs-module.hをincludeするんだから、そっちになんかいい感じにまとめておいてくれたほうが、もっとうれしい。

ポインタの受け渡し

引数渡して、処理した結果を返しておわり、という純粋な関数だけでなく、継続して処理したいという場合があると思います。
今回作ったパケットキャプチャもそうで、init関数で、デバイスをオープンしたりパケットキャプチャを開始して、get関数を呼ぶたびにinit関数で初期化したところから情報をとってくるようになってます。

init関数では初期化情報をemacs側に返却、get関数では返却された情報を返して、それを元にライブラリ側は処理をする必要があるわけです。

もちろん、その辺もできるようになってまして、emacsにポインタを返却する場合は、

return env->make_user_ptr(env, free, handle); //handleがポインタ

https://github.com/grugrut/emacshark/blob/master/emacshark.c#L79

emacsから受けとる場合は、

  pcap_t *handle = env->get_user_ptr(env, args[0]);
  if (env->non_local_exit_check(env) != emacs_funcall_exit_return) {
    return env->intern(env, "nil");
  }

https://github.com/grugrut/emacshark/blob/master/emacshark.c#L88-L91
でいけます。受けとるだけなら、get_user_ptrだけでよいのですが、その後のif文で、問題ないかチェックしていて、おかしいポインタがわたされた場合は、emacs側にもエラーメッセージをミニバッファに表示させることなどができるようになっています。
これができることに気付かず最初開発中は、セグメンテーション違反(通称セグフォ)でemacsごと落ちまくって大変でした。
make_user_ptrの2番目の変数は、開放時に呼ばれる関数のようです。freeしてるだけですが実はlibpcapなんでもろもろcloseしないといけないのでは、、、?という疑惑があります。

所感

もうちょっとカジュアルに使えるものかと思ったけど、結構面倒。とはいえ、だいたい雛形決まってるので、一度書いてしまえば、Makefileソースコードも半分以上は使い回せる気がする(だからこそ、本体側で吸収してくれればもっとカジュアルに書けるのでは、ということでもある)

emacshark自身の展望としては、フィルタを設定できるようにする、とか、(おそらくバグで)initメソッド呼んだとき、ひとつパケットを受けとるまで待機状態に入ってしまうので、その辺をなんとかする、とか、もうちょっと一般的に作れたらmelpaへの登録を試みる、とか、野望は幅広く持っています。

いっけんとっつきにくい、というか前提が多すぎで本質でないところにとらわれてしまうのですが、なんちゃってで書いても、なんだかんだ動くもの作れる(このemacsharkもバグ修正とか細かいところ除けば大筋は、いちからでも2時間ぐらいでできました)ので、ぜひなんかおもしろパッケージが出てくること楽しみにしています。

明日は、ncaqさんです。自作のパッケージを公開するとのことらしいので楽しみですね。

*1:余談ながら、去年のadvent calendarでは、下から二番目のxwdigetについて書きました