slackと仲良くなる
まず、公式ドキュメントよめ
公式読まない人は何をやっても使いこなせないでしょう。と自分にいいきかせ読んでみたら、とても良かったので使えそうなやつをカテゴリ別にメモしていきます。
Hipchat → Slack移行なう
もともとHipchatを使っていて、先月くらいから移行中。複数のチームを共存できるのイイ。個人(家用)と会社用でつかっています。
個人のアカウントはIFTTT連携して、Google App Scriptから前日に翌日は「なんのゴミの日なのか?」をSlackに通知するために使ったり。
本格的につかうならショートカットとスニペットは覚えなくては
Hipchat使ってた時には⌘ + t
と⌘ + [
, ⌘ + ]
くらいしか使ってなかったのですが、公式に載ってるショートカットがなさすぎる。
それに対し、slackはチャンネル切り替えなどの基本動作だけで15個。スバラ!
設定
Display Options
✅ show times with 24-hour clock → 24時間表記(個人的にこっちがすきなので)
Advanced Options
✅ Whtn typing code with ```, Enter should not send the message → コードを入力しようとした時はEnterでメッセージを送らない。(対となる```で閉じたあとにEnterで送信は有効化される)
✅ List public and privte channels separately → 公開と非公開のチャンネルのリストを分ける
ショートカット
Chromeとショートカットが同じ or 似ていて覚えやすいショートカット
コンテキストスイッチが少ないのでありがたい。
Chrome | Shortcut | Slack |
---|---|---|
拡大/縮小/元の縮尺 | ⌘ + + ⌘ + - ⌘ + 0 |
拡大 縮小 元の縮尺 |
ブラウザフォワード/バック | ⌘ + ] ⌘ + [ |
1つ先/前に見ていたチャンネル |
カレントタブのリロード | ⌘ + R |
カレントアカウントのリロード |
N番目のタブを開く | ⌘ + [1-9] |
N番目のチームを開く |
次/前のタブを開く | ⌘ + shift + ] ⌘ + shift + [ |
次/前のチームを開く |
形式に合わせてペースト | ⌘ + shift + v |
スニペットペースト |
設定を開く | ⌘ + , |
設定を開く |
あまり使わないショートカットがあるにせよ、これだけで覚えやすい。
⌘ + ,
に限っては当然のごとく。
その他に積極的に使うショートカット
Shortcut | 動作 |
---|---|
Alt + ↑ Alt + ↓ |
上/下のチャンネルを開く |
Shift + Alt + ↑ Shift + Alt + ↓ |
未読がある上/下のチャンネルを開く |
Esc |
開いているチャンネルを既読にする |
⌘ + f |
開いてるチャンネルをターゲットに検索する |
⌘ + / |
ショートカットチートシート表示 |
Alt + messageをクリック |
そこから未読扱いにする。 後で読む的な使い方 |
⌘ + Shift + m |
自分へのメンションのリストを開く |
/
コマンド
コマンド | 動作 |
---|---|
/leave |
チャンネルから退出 |
/keys |
ショートカットチートシート表示 |
/who |
チャンネルにいる人をリスト表示 |
/search query |
検索 |
searchのクエリについて
どこのだれからか
in:[channel|user]
from:[user|me]
メッセージに含む
has:[link|star|:絵文字:]
いつ
befor:
after:
on:
during:
時間指定に使えるのは
- today
- yesterday
- week
- month
- year
- YYYY-MM-DD
ここは少し、わかっていないところ。
まとめると覚えるのでよい。
Vagrant公式ドキュメントを見るときに気をつけたい
VagrantのprovisionerにAnsible
Vagrantにはprovisionerが幾つか、用意されています。
メジャーなのはchef-client
やchef-solo
かと思います。vagrant-omunibusというプラグインがあるので、実行環境を整えやすいですし。
また、簡単なプロビジョニングであればshell
で済みます。
一方で、自分はよくAnsibleを使っています。最近Ansible熱があるというだけです笑
OSXの環境を整えるのもAnsibleでやったりしています。
話は戻りまして、Vagrant上にDockerのプロビジョニング環境を作るときにもprovisionerにAnsibleを使いました。
この時、公式ドキュメントを見ていて、設定項目がすくなくないか?と思ったのですがVagrantにはAnsibleとAnsible localの2種類があるため、共通のオプションは別ページとのこと。
Optionのリストだけを見ていて、説明をすっ飛ばしたので気づかなかった。。。
Ansibleの共通オプションはCommon Ansible Options - Provisioning | Vagrant by HashiCorpを見るべし。また、気をつけたいのがこのページはprovisionerのカラムからは直接飛べないので、一度Ansibleのページを開き、OPTIONSの説明の
This section lists the specific options for the Ansible (remote) provisioner. In addition to the options listed below, this provisioner supports the common options for both Ansible provisioners.
この部分のリンクを開かねばなりません。
副産物
説明がないものと思い込み、Vagrantのgithubのソースを読んでおりました。
この部分を読めば、オプションが書いてあります。また、他のprovisionerも同様です。
DocsへはPRできる模様
VagrantのDocsの右下にEdit this page
というリンクが有り、クリックするとgithub上で対応するページのドキュメントのソースに飛びます。きっと、PRを受け入れてくれるのでしょう。
https://github.com/mitchellh/vagrant/tree/master/website/docs
個人的にこういうスタンスになっている事自体が、すごいな〜と思いました。
壁美人をつかったという話
「C++初心者ならビルドはBazelでラクしちゃいましょう」の主役となったBazelや「100円でオフィスのデスクをスッキリさせたという報告。」でまな板たて使うというアイディアを教えてくれた、会社の先輩に年末に
「壁美人いいよ。特に賃貸だったら」
と言われ、我が家でも導入してみた。
壁美人とは
一言で言うと、画鋲や釘など大きな穴を開けずに壁にフックをつけたり、テレビを設置したりできるというもの。
画鋲でも穴がひどいと、壁紙張り替えになって敷金なくなっちゃう場合があるので。。。
パッケージがこれ
ホチキスでとめる
まさかのホチキス。家にあるので、工具も特別なものが不要なところがとてもありがたい。
試しにつけてみる
拡大してみる
比較用に画鋲も刺してみたけど、画鋲のあなは目立つ
ホチキス針は専用針
完全に憶測なのですが、ホチキスの芯はステンレス。なぜなら、文房具屋で売ってるものは鉄なのでさびやすいです。
サビで壁が赤くならないようになってるんですね〜
見た目が不格好じゃない?
自分もそう思いましたが、隠せました!
もっとリース飾りたいと言われたので、追加購入します
Mac OSXの設定をAnsibleで9割以上自動化する
自分のMBPRetinaは3年前のモデルでアップデートする度に、スリープからの復帰画面がおかしくなったりしたのでOSのクリーンインストールも兼ねました。データはほとんどクラウド化されてるので残るは設定ファイルのみ。0から設定するのだるい。
今後、0スタートするときも苦労したくない!ポチポチクリックしたくない!ということで
この記事にインスパイアされて、自分もやったので、ハマりどころとかプラスで対応したところをメモ。 9割はこれで行ける、残り1割はアプリにログインした時にクラウド同期してくれる系(Chromeとか).
githubにdotfilesやらshellにするよりも比較的容易にprogramaticallyにできたのでやってよかった。
対応したこと
- ansibleのインストール
- brewのインストール
- brewによるアプリのインストール
- brew-caskによるアプリのインストール
defaults
コマンドによるMacの設定自動化- zshの設定
- vimの設定
- gitの設定
- sshの設定
- xcodeのプラグインパッケージマネージャのインストール
xcodeインストールして利用規約に許諾したあとに
cd /path/to/repository-top && make
でプロビジョニングが終わるようになっています。
ディレクトリ構成
provisioning ❯❯❯ tree -L 1 . ├── Context.sh ├── InstallAnsible.sh ├── Makefile ├── PlayAnsible.sh ├── README.md └── provisioning/
- Context.sh ... 実行環境のコンテキスト
- InstallAnsible.sh ... pipがなければインストール、ansibleがなければインストール
- Makefile ... InstallAnsible, PlayAnsible.shのフック
- PlayAnsible.sh ... Ansibleの実行
- provisioning ... Ansibleの設定集
brewのインストール
はじめに紹介したリンクではbrewでansibleをインストールしており、brewがある前提でansibleが組まれています。
もっと自動化したかったのでansibleを先にインストールし、brewのroleのタスク内でインストールするようい順番を変えました。
Ansibleのインストール
InstallAnsible.sh
which pip >/dev/null 2>&1 if [ $? -ne 0 ]; then echo "not found command pip" echo "install pip" sudo easy_install pip fi which ansible >/dev/null 2>&1 if [ $? -ne 0 ]; then echo "not found command ansible" echo "install ansible" sudo pip install ansible fi
easy_installでpipをインストールし、pipでansibleをインストールしています。自分の環境ではsudoを付ける必要がありました。
改めてbrewのインストール
roles/brew/tasks/main.ymlの一部抜粋
- name: update brew register: result ignore_errors: True command: > brew update - name: get brew installer get_url: url=https://raw.githubusercontent.com/Homebrew/install/master/install dest=/tmp/brew_install when: result|failed - name: proceed brew installer command: > ruby /tmp/brew_install when: result|failed
brewのアップデートに失敗すればコマンドがないという前提のもと、なければbrewをインストールしています。
brewや後述するXcodeのAlcatraz、VimのNeoBundleのようにインストールスクリプトが準備されていると、楽ですね〜。
あとは、インストールするアプリを列挙するのみ
上記がbrew-caskでインストールできるアプリのリストですが、cloneしないと、全アプリのリストを確認できません。面倒な場合は、リポジトリ内検索でしらべるとヒットしてくれたりします。
defaultsコマンドによる設定値の変更
http://docs.ansible.com/ansible/osx_defaults_module.htmlによると
- osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present
osx_defaults
モジュールがある!Ansible先生有能すぎ!
ただし、Ansible2.0系以上で使えるのですがまだrc版で、2016年1月時点ではpip install ansible
でインストールされるstableなバージョンは1.9.4です。
したがって、実行するとモジュールがないと怒られてしまいます。
ansible --version
で取れるバージョンのメジャーバージョンが1ならcommand、Ansibleが2.0系ならosx_defaultsによる実行するタスクを用意しroleを分け、playbook_defaults.ymlとplaybook_defaults_v1.ymlを呼び分けるようにしました。
defaults内のmain.ymlは共有したいのでシンボリックリンクで対応。
本当は1つのroleにして、ansible_version.major
で取得した値でtaskのincludeを切り替えようと思ったのですが、includeされた設定はすべて構文チェックをされるらしくだめでした。
また、ansible_version
という変数があること自体は公式ドキュメントに記載されておらず、このissueで知りました。2系でundefinedになってしまうとのことだが、修正されているようです。
defaultsの設定
roles/defaults/defaults/main.ymlの一部を抜粋
# # see also https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/defaults.1.html # mac_defaults_settings: - { domain: com.apple.ImageCapture, key: disableHotPlug, type: bool, value: false, } mac_global_domain_defaults_settings: - { key: InitialKeyRepeat, type: int, value: 13, } - { key: KeyRepeat, type: int, value: 3, }
設定が増えてきたらドメイン別にファイルを分けて、includeするといいかも。
global_domainの設定に関してはドメイン指定が不要なので別リストとして持っています。あとは、これらをループして設定するタスクを書けばOK
defaults_v1/tasks/main.yml
- name: command: > defaults write {{ item.domain }} {{ item.key }} -{{ item.type }} {{ item.value }} with_items: mac_defaults_settings when: mac_defaults_settings - name: command: > defaults write -g {{ item.key }} -{{ item.type }} {{ item.value }} with_items: mac_global_domain_defaults_settings when: mac_global_domain_defaults_settings
一部抜粋でしたのが設定できる値はいろいろあるので、こちらも参照ください
zshのインストールと設定
roles/zsh
ではzshのプラグインマネージャantigenとpreztoのインストールを行っています。
ここはハマりました。antigenの設定ファイルと読み込むようにして、ログインシェルとしてzshを設定したあとでもantigenコマンドがnot foundに。
したがって少し強引にantigenコマンドを呼んでいます
- name: append source antigen shell: > echo "{{source_antigen}}" >> ~/.zshrc when: grep_command.rc != 0 - name: install prezto through antigen shell: > source ~/.zshrc && antigen bundle sorin-ionescu/prezto executable={{which_zsh.stdout}} ignore_errors: True
zshをログインシェルに設定
以下の設定ではログインシェルにzshを設定しており、 このときにパスワードの入力が必要です。パスワードの入力に失敗しても3回までリトライするようにしています。
- name: change login shell shell: > chpass -s {{which_zsh.stdout}} register: result until: result.rc == 0 retries: 3
パスワードはタスクの序盤で入力が要求されるのでの直前に
osascript -e 'display notification "Prease input password in terminal" with title "osx-provisioning"'
を実行してあげると
こんな感じで、notiをしてくれます。
sayコマンドで喋ってもらうのもいいですね(笑)
xcodeのプラグインパッケージマネージャ(Alcatraz)のインストール
これはダウンロードしてシェルを実行するだけでした。
sshの設定
鍵をリポジトリに含むわけにも行かないので、ここはほぼ手作業。.sshで~/sshにシンボリックリンクを貼るようにしました。
sshディレクトリにプライベートリポジトリを配置すれば良いかなと。submoduleにしなかったのはpublicリポジトリからprivateリポジトリに依存するのは微妙だなと思ったので。
設定ファイルのImport
ほとんどのアプリでは設定のexportとimport機能がついています。例えば、karabenerは設定ファイルをshellとしてexportしてくれるので、exportしておいてkarabenerをインストール後にshell実行すれば設定を復元することができます。
こんな感じ
$ /Applications/Karabiner.app/Contents/Library/bin/karabiner export #!/bin/sh cli=/Applications/Karabiner.app/Contents/Library/bin/karabiner $cli set remap.doublepresscommandQ 1 /bin/echo -n .
今回、自分はこの設定たちをexportする前にSSDのクリーンをしてしまったので後悔。これから作っていく予定。
まとめ
これでPC買い替えも、会社のPCの環境構築も、クリーンインストールも怖くない!!そして、まだまだdefaultsの設定が足りてないのでやらねば!
参考
ブログなど
公式docks
please fork me!
追記
brew caskでインストールしていたアプリを幾つか、AppStore経由でインストールできるように改修した。
OSXのコマンドラインからすると捗った設定リスト
上記で紹介されていたiPhoneを接続した時に写真アプリの自動起動を防ぐターミナルでのコマンド
defaults write com.apple.ImageCapture disableHotPlug -bool NO
これ便利。実は他にもある。さらに、コマンドライン経由から設定することで、設定アプリではUI上不可能な値も設定できるので設定してまとめてみた。
フォーマット
まずはdefaults
コマンドのフォーマットから
一般的なフォーマット
サードパーティ製のアプリも含め、Dockの設定などがこちら。
書き込み
defaults write DOMAIN KEY -TYPE VALUE
読み込み
defaults read DOMAIN
グローバルドメイン
グローバルドメインと呼ばれる領域がある。これはジェネラルな内容、キーボードのキーリピート設定やマウスのカーソル移動速度などが設定される領域。
書き込み
defaults write -g KEY -TYPE VALUE
読み込み
defaults read -g
設定できるドメインを調べる
man defaults
しましょう。マニュアルを読もう。
defaults domains
↑を実行すると設定可能なドメインのリストの取得ができます。
~/ ❯❯❯ defaults domains 2BUA8C4S2C.com.agilebits.onepassword4-helper, MobileMeAccounts, com.agilebits.onepassword4, com.alfredapp.Alfred, com.apple.AddressBook, com.apple.AppleMultitouchMouse, com.apple.AppleMultitouchTrackpad, com.apple.BezelServices, com.apple.CalendarAgent, com.apple.CallHistorySyncHelper, com.apple.CharacterPicker, com.apple.CloudPhotosConfiguration, com.apple.CommCenter.counts, com.apple.CoreGraphics, com.apple.Dictionary, com.apple.FolderActionsDispatcher, com.apple.GEO, . . .
したがって、defaults read domain
して設定したいアプリのドメインを調べて目的のキーのバリューを変更すればいいことになります。
全部の設定値をダンプするワンライナー
echo $(defaults domains) | tr -s ',' '\n' | while read domain; do; defaults read $domain; done;
設定を反映する
Finderなどは設定がキャッシュされていて、プロセスを再起動しないと反映されません。
基本的にはkillall APPLICATION_NAME
でいけます。Finderならkillall Finder
また、KeyboardのKeyrepeat設定は再起動不要、Trackpadはプロセスを見つけられず再ログインで対処しています。
Trackpad周り
タップでクリックを許可
defaults write com.apple.driver.AppleBluetoothMultitouch.trackpad Clicking -int 1
カーソルの移動速度を変える
defaults write -g com.apple.trackpad.scaling -float 1
遅 0 ~ N 速
隠しファイルをFinder上で可視化する
defaults write com.apple.finder AppleShowAllFiles true
すべての拡張子を表示する
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
Dock
Dockの表示位置
defaults write com.apple.dock orientation -string "left"
left, bottom, rightが設定可能
オンマウスでのアイコンサイズズーム
defaults write com.apple.dock magnification -bool true
Dockを自動的に隠す
defaults write com.apple.dock autohide -bool true
アイコンサイズの変更
defaults write com.apple.dock tilesize -int 50
KeyRepeat
InitialKeyRepeat
defaults write -g InitialKeyRepeat -int 13
速 0 ~ N 遅
KeyRepeatが始まるまでのディレイ。設定アプリではmost fastで15までしか設定できないです。
KeyRepeat
defaults write -g InitialKeyRepeat -int 3
速 0 ~ N 遅
KeyRepeat中のディレイ。設定アプリでは2->6と設定値が飛んでしまうので1単位で設定できないです。
参照
ここを参照すればいろいろ便利に設定してるし、設定の説明をしてくれてる方々がいるのでおすすめ〜
応用
こんな感じでコマンドライン化することによって設定を定義して置けるようになるのが副産物。
Ansible化したのがこちら
追記
どこのことだろう?> "ここを参照すればいろいろ便利に設定してるし、設定の説明をしてくれてる方々がいる"
参照先を間違っていたので、修正しました。ご指摘ありがとうございます〜!
Vagrant上でDockerコンテナプロビジョニングとDockerコンテナプロビジョニング環境のプロビジョニングにAnsibleを使う
タイトルが長い笑
この記事はQiita Advent Calendar 2015 23日目の記事です。 前日はunchemistさんのここがヘンだよAnsible (ハマりどころと対策について)でした。
Ansibleのカレンダーが空いてたので投稿いたしましたが、もしかしたらPackerとかDockerの割合が多いかもしれませんが、ご容赦くだされば幸いです!!
今回は、
- Vagrant上でPackerでDockerコンテナのプロビジョニング環境整備をプロビジョニングする際にAnsibleを使う
- 1で作った環境上で、さらにPackerでDockerコンテナをプロビジョニングするさいにAnsibleを使う
という2点を行ったメモを紹介します。
記事の対象者
- Ansible, Docker, Packerを触りはじめたばかり
全体像
環境
Apps | Version |
---|---|
Mac OSX | 10.11 |
VirtualBox | 5.0.10 r104061 |
Vagrant | 1.8.0 |
Docker | 1.7.1 |
Ansible | 1.7.1 |
Packer | 0.8.6 |
まず、MacOSX上では狭義の意味ではDockerは動かないですが Installation on Macの通りにすれば、ハイパーバイザー上にDockerをホスティング&フックするラッパーツールを使うことができます
しかし、自分の環境では、Packerでプロビジョニング中に進まないという現象に陥いったことや、Vagrant完結にしてホストを汚さないようにしたかったためVagrant上で構築しました。
また、Centos6系ではkernelが2.6ですが、Dockerを起動するには3系である必要があります。
Vagrant起動時にDockerコンテナプロビジョニング環境を整える
DockerコンテナプロビジョニングにはPackerを使うので、具体的にはCentosにDockerとPackerをインストールします。
AnsibleによるVagrantBoxのプロビジョニングポイント
Vagrantfile
にAnsibleでプロビジョニングする旨とplaybook.ymlを指定すればOK
あとは、playbookとrolesを拡充していくのみです。
サンプル
https://github.com/matsuokah/vagrant-docker-provisionergithub.com
git clone git@github.com:matsuokah/vagrant-docker-provisioner.git
してディレクトリでvagrant up
すればpackerが使えるイメージが立ち上がります。冪等性は担保してないのでご注意ください。
ファイルツリー
├── README.md ├── Vagrantfile └── provisioning ├── playbook.yml └── roles ├── common │ └── tasks │ └── main.yml ├── docker │ ├── handlers │ │ └── main.yml │ └── tasks │ └── main.yml └── packer ├── files │ └── packer_0.8.6_linux_amd64.zip └── tasks └── main.yml
VagrantfileがあるディレクトリにAnsibleの設定を置いただけですね。
Vagrantfile
# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(2) do |config| config.vm.box = "centos7" config.vm.box_url = "https://github.com/holms/vagrant-centos7-box/releases/download/7.1.1503.001/CentOS-7.1.1503-x86_64-netboot.box" # provisioning config.vm.provision "ansible" do |ansible| ansible.playbook = "provisioning/playbook.yml" ansible.sudo = true end end
Ansibleのモジュール実行の前提をしらずにハマる
Ansibleではモジュールを実行するためには、そのモジュールで使われるコマンドがインストールされているという前提を満たす必要があります。 例えばUnarcive moduleでは
Note
requires tar/unzip command on target host
のように、ホスト上でtar
及び、unzip
コマンドが使える必要があると言及しています。
TASK: [packer | Unarchive packer to installation directory] ******************* failed: [default] => {"failed": true} msg: Failed to find handler to unarchive. Make sure the required command to extract the file is installed.
上記のようなエラーが出た場合、commonにはそれらの前提を整えるプロビジョニングを書くなどするといいかもです。これを知らないと、ただモジュールを実行しているだけなのに・・・とすこしハマリます。
PackerでDockerコンテナのプロビジョニングを行う
サンプル
https://github.com/matsuokah/packer-container-provisioner-samplegithub.com
後は、先程作ったpackerが実行可能なbox上で↑をcloneしてpacker build template.json
とするだけです。
template.json
{ "_comment": "Docker provisioner", "builders": [ { "type": "docker", "image": "centos:7", "export_path": "/vagrant/image.tar" } ], "provisioners": [ { "type": "shell", "inline": [ "yum localinstall -y http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm", "yum install -y python-pip python-devel libstdc++ gcc gcc-c++ kernel-devel", "pip install ansible" ], }, { "type": "ansible-local", "playbook_file": "ansible/playbook.yml", "role_paths": ["ansible/roles/common", "ansible/roles/nginx"], "staging_directory": "/tmp/docker-packer-provisioner" } ] }
やっていることは、
- centosコンテナをpull
- Ansibleをインストール
- Ansible実行
例ではnginxをインストールしてみました。
/vagrant/image.tar
としてexportしたので
$ cat /vagrant/image.tar | docker import - centos_nginx:latest $ docker run -d -i -t --name my_nginx centos_nginx:lagest "/bin/bash"
とでもすればnginxが起動できるかと思います。
動作確認するためにはVagrantfile
でdockerへのポート転送のすればできるでしょう。
以上、「Vagrant上でDockerコンテナプロビジョニングとDockerコンテナプロビジョニング環境のプロビジョニングにAnsibleを使う」でした!!
リファレンス
Cocos2d-xにおけるOS依存機能利用のための実装
この記事はqiita advent calendar cocos2d-x 2015の22日目の記事です。 qiita.com
C++完結はできない
Cocos2d-xでは主にクロスプラットフォームな開発を期待して選ぶ方が多いと思います。
しかし、プラットフォーム依存な所、例えば文字の入力、課金、OS情報やユーザー情報へのアクセスをする場合にはそのプラットフォームで提供されているSDKを書く必要が出てきます。
また、Cocos2d-xで書くよりもOS依存のSDKの力を借りてネイティブで書いたほうがパフォーマンス向上に苦労しないケース(スクロールビューやGridView, CollectionViewなど)も少なくない上に、ユーザーにとっても使い慣れたUIを提供することができるというメリットもあります。
iOSの場合はObjective-CがC++への互換があるため、Objective-C++として書けばほとんど苦労せずにプラットフォーム依存な部分にアクセス可能ですが、Androidの場合はJavaでAndroidSDKをつかって書く必要がありますね。
今回はこのAndroidSDKをC++からどう使うか?そして、開発を加速させるためにネイティブを使う側(C++)からJavaもObjective-Cも同じインタフェースで呼べるようにするための設計をまとめていきます。
JNI
まずはC++からJavaを呼び出すにはJNIという知識が必要になります。一言で言えばJavaと他の言語をブリッジする機能です。
例と簡単な説明は以下に挙げますが、詳しい説明はOracleのJNIのリファレンスを読んでください。
Calc.java
package jp.matsuokah; class Calc { public static int add(int x, int y) { return x + y; } }
上記のadd
をC++から呼ぼうとするにはいくつかのステップが必要です。
一番簡単なパターンで説明します。
Cocos2d-xで用意されているJNI関連のユーティリティを使った場合
struct calc { int add(int x, int y) { cocos2d::JniMethodInfo methodInfo; if (!cocos2d::JniHelper::getStaticMethodInfo(methodInfo, "jp/matsuokah/Calc", "add", "(I)I")) { throw std::runtime_error{"method couldn't found"}; } auto result = methodInfo.env->CallStaticIntMethod(methodInfo.classID, methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); return result; } }
順に流れを説明すると
- 文字列で定義したクラス名、メソッド名、メソッドのシグネチャを手がかりにstatic methodの情報を取得
- なければ例外を投げる
- メソッドをコールする
- JNI関連のオブジェクトを開放する
- 返却
- 文字列で定義したクラス名、メソッド名、メソッドのシグネチャを手がかりにstatic methodの情報を取得
これだけで、なんじゃこりゃ状態ですよね。
特にメソッドの情報を取得するところや、オブジェクトの開放は書くたびに頭を悩ませてくれそうです。
シグネチャが変わっても文字列なので、ランタイムでエラーになるまで気づかないかもしれません。
しかし、クラス名やメソッド名の検出はできないものの、シグネチャはテンプレート引数から自動生成、オブジェクトの開放はstd::unique_ptr
を使えば意識しなくて済みそうです。
ということで、業務ではcocos2d-xのJniHelperを置き換えるライブラリを開発しました。(公開はできませんが擬似コードで方針を幾つか書きたいと思います)
今では例えば、WebViewを使う際には
void WebViewRef::load(const std::string& uri) { auto env = get_env(); jni::call_method<void>(env, this->ref_.get(), webview::load, uri); }
上記はuriをロードする場合のメソッドです。リターン値、メソッド名、メソッドの引数を指定するだけで済むようになったのでJavaのメソッド追加やシグネチャの追加がとても楽になりました。
ブリッジ部は他のOSも絡むので以下にiOSとAndroidを含めたクラススタックを載せ、概要を説明した後で、詳細を説明します。
iOS/Android共通WebViewの全体像
共通インタフェース
void* native_ptr
としてAndroidはRef、iOSはControllerのポインタをもつ- ネイティブから伝搬する各イベントハンドラをセットできるようにしている
- 元の共通インタフェースのthisポインタをRef及びControllerに渡すことで、ネイティブからイベントハンドラを発火する
共通インタフェースの各翻訳単位の実装
void* native_ptr
を各翻訳単位でキャストしてメソッドをコールする
Refクラス(Androidのみ)
AndroidのJNI関連のラッパークラス。JNIオブジェクトの確保/開放、メソッドやクラス情報の取得をテンプレート実装したもの
ネイティブ実装
Androidの場合の擬似コード
WebView.cpp
using ref_t = WebViewRef; WebView::WebView() ptr_:{new WebViewRef{this}} {} WebView::~WebView() {delete ptr_;} void WebView::load(const std::string& url) { static_cast<ref_t*>(this->ptr_)->load(url); } bool WebView::isLoading() { return static_cast<ref_t*>(this->ptr_)->isLoading(); }
WebViewRef.hpp
#include ... namespace android { class WebViewRef { ref_wrapper ref_; // ref_wrapperはjavaオブジェクト用のカスタムデリータを定義したunique_ptrのエイリアス WebViewRef(const void* ptr); ~WebViewRef(); void load(const std::string& uri); bool isLoading(); } } JNIEXPORT void JNICALL Java_jp_matsuoka_
WebViewRef.hpp
#include ... namespace android { // シグネチャ関連 struct webview { // qualified_nameはクラス名 static constexpr auto qualified_name = "jp/matsuokah/Webview"; // 以下メソッド名 static constexpr auto load = "load"; static constexpr auto isLoading = "isLoading"; . . . } WebViewRef::WebViewRef(const void* ptr) { auto env = get_env(); this->ref_ = make_ref(env, make_object<webview>(env, reinterpret_cast<std::int64_t>(ptr))); } WebViewRef::~WebViewRef() { this->ref_ = nullptr; } void WebViewRef::load(const std::string& uri) { auto env = get_env(); jni::call_method<void>(env, this->ref_.get(), webview::load, uri); } bool WebViewRef::isLoading() { auto env = get_env(); return jni::call_method<bool>(env, this->ref_.get(), webview::isLoading); } // 他のメソッドも同様に定義する . . . }
WebView.java
package jp.matsuokah; import ... class WebView { private long ptr; private native void onLoad(long ptr); WebView(long ptr) { this.ptr = ptr; } void load(String uri) { } boolean isLoading() { return true; //スタブ実装 } }
WebViewRef.cpp
ではjni関連のライブラリをフル活用しており、実際にコールするところの実装は2行で済んでいます。
と、AndroidではこのようにネイティブとCocosの共通的なブリッジ部を開発しておりました。iOSはC++の感覚で実装できるので紹介は省きます。
最後はほぼ擬似コードにはなってしまいましたが 共通インタフェースで各プラットフォームのAPIをコールできそうな雰囲気は感じ取っていただけたかと思います。
このようにフレームワークっぽくしておくと、追加実装になった時もそれに準じて書くだけで、ブリッジしていけるので個人的には開発効率化ができたなーと感じています。
書いていて気付きましたが、pimplパターンを使えば、共通インタフェースをもう少し上手く書けそうですね。
C++初心者ならビルドはBazelでラクしちゃいましょう
この記事は 初心者C++er Advent Calendar 2015, Qiita C++ Advent Calendar 2015 15 日目の記事です.
注意
- UbuntuもしくはMac OSXの環境が作れない方はそっと閉じてください。(VagrantでUbuntuはすぐ準備できる)
- スマホアプリ畑の人間が書くので、それに向けた内容も多くなっています。
- PCからの閲覧をおすすめします。スマホで読むと、コードのレイアウトが崩れるようなので。
ビルドツールについて
みなさんはビルドオートメーションツールには何を使っていますか?
玄人界隈ではcmakeが定番です。
私が携わっているプロジェクトではgypと呼ばれるツールを使っています。
元々Chromiumで開発されていたGoogleのオープンソースの一つで、node.jsにも使われているビルドツールです。
特殊なフォーマットに従ってファイルやマクロを定義することで、設定ファイルに応じたmakeファイルやXcodeなどプロジェクトを吐き出してくれる中間的なツールで、今は開発が進んでいないようです。
さて、前振りが長くなりましたが、このgypに取って代わる次世代ビルドツールとして開発されているのがBazelです。
BazelはCookpadの技術ブログでも取り上げてられているように、C++だけではなくスマホアプリや複数言語のビルドにも対応しているので、延長線上にポテンシャルがあるという観点も含めてBazelをピックアップしました。
また、話題の機械学習フレームワークTensorFlowもBazelでビルドされてます。
少しの設定ファイルを書けば機能単位でライブラリ化や依存関係を定義できるので、初心者はBazelでラクしましょう!!!
サンプル
↑を元に、解説していきます。
Hello World
インストール方法は環境によって違うのと、簡単なので割愛します。
HelloWorldプロジェクトのファイル構成
HelloWorld ├── Makefile ├── WORKSPACE └── src ├── BUILD └── main.cpp
注目すべきはWORKSPACEとBUILDです。
WORKSPACE
The location of the workspace directory is not significant, but it must contain a file called WORKSPACE in the top-level directory. The WORKSPACE file may be an empty file, or it may contain references to external dependencies required to build the outputs.
Getting Started with Bazelより引用
「WORKSPACEという名前のファイルがプロジェクトのトップとなるディレクトリに必要で、他のプロジェクトへの依存性を定義するもの。依存性がなければ空ファイルとしておいておけばOK」とのこと
今回は、外部のソースに依存しないのでWORKSPACEは空ファイルです。
TensorFlowのWORKSPACEでは、外部に依存する設定が定義されていますね!
zipをダウンロードしてhashチェックしたり、gitのリポジトリをhash指定で持ってきてくれたりしてくれるので使い勝手が良さそうです!
BUILD
Bazel figures out what to build by looking for files named BUILD in your workspace
Getting Started with Bazelより引用
「BazelはBUILDと呼ばれるファイルからビルドすべきファイルを割り出す」とのこと。
つまり、WORKSPACEが外部のソースに対する依存関係の設定、BUILDが具体的なビルドの設定であり、両方ともビルドに欠かせない設定ファイルということになります。
それでは早速設定を見ていきましょう。
main.cpp
#include <iostream> int main() { std::cout << "Hello World" << std::endl; return 0; }
まずは、ソースから。シンプルなHello Worldなので解説は不要ですね。
src/BUILD
cc_binary( name = "main", srcs = ["main.cpp"], )
main.cpp
をmain
というターゲット名で実行ファイルを作成するという設定になります。nameのみがrequiredなパラメータです。
Makefile
all: run BAZEL=$(shell which bazel) .PHONY: run run : build $(BAZEL) run //src:main .PHONY: build build : $(BAZEL) build //src:main .PHONY: clean clean : $(BAZEL) clean
BazelはURIスキームライクにパッケージを定義しており、それらをdependencyとして扱うことができます。 そして、サブコマンドの後にパッケージを指定することでビルドターゲットとしてビルドを開始することができます。
Makefile内に定義してある//src:main
はsrcディレクトリ以下にあるBUILDファイルに定義されているmainというターゲット名を示していることになります。
実行してみる
make run
を実行すればsrc/BUILD
に定義されているmain
がビルドターゲットとなり、main.cpp
がコンパイルされ、main
ターゲットが実行されます。
失敗させてみる
セミコロンを抜いてみました
実行後のディレクトリ
HelloWorld ├── Makefile ├── WORKSPACE ├── bazel-HelloWorld ├── bazel-bin ├── bazel-genfiles ├── bazel-out ├── bazel-testlogs └── src ├── BUILD └── main.cpp
このように、bazel
というプレフィックスでビルド関連のディレクトリ郡が生成されます。自動生成されたディレクトリやファイルは、bazel clean
で一挙に削除することができます。
HelloWorldプロジェクトではMakefileにエイリアスを定義したのでmake clean
で同様のことが可能です。
static libraryの依存関係を定義し、mainから呼び出してみる
HelloWorld出力部分をstatic library化し、//src:main
から依存関係を解決して、コールしてみます。
HelloWorldStaticLibのプロジェクト構成
. ├── Makefile ├── WORKSPACE └── src ├── greeting │ ├── BUILD │ ├── hello.cpp │ └── hello.hpp ├── BUILD └── main.cpp
Makefile
とWORKSPACE
は全く一緒です。
また、hello.hpp,hello.cpp,main.cppはHello World
の出力を関数化しただけなので記載を割愛します。
src/greeting/BUILD
cc_library( name = "greeting", srcs = glob(["*.cpp"]), hdrs = glob(["*.hpp"]), visibility = ["//visibility:public"], linkstatic = 1, )
あらたな設定が出てきましたが、雰囲気でおわかりかと思います。
まず、cc_library
という単位に変わりました。
さらに、srcsがファイルの列挙ではなくglob
関数を使っています。
お察しの通りワイルドカードでコンパイルターゲットとなるファイルを指定しています。hdrsも同様です。
visibility
はそのビルド単位の可視性となります。適切に設定することでビルド単位間の結合性を疎にできるので、必要なcc_libaryだけをpublicにすることが好ましいです。
publicにしたことで、src:main
からsrc/greeting:greeting
が可視化されています。
linkstatic
はフラグです。
src/BUILD
cc_binary( name = "main", srcs = glob(["*.cpp"]), deps = ["//src/greeting"], )
deps
という設定が入りました。greetingライブラリに依存してるんですね〜。
実行してみる
実行結果は変わらないので割愛して、変わったところをば
~/p/h/HelloWorldStaticLib ❯❯❯ ls bazel-out/local_darwin-fastbuild/bin/src/greeting/ _objs libgreeting.a
libgreeting.aができていますね!
と、こんな感じでstatic libraryやshared libraryをつくれて依存関係もよしなにしてくれるので、コーディングに時間を割きたい初心者C++erにはおすすめなのではないでしょうか。
gitignore
*.swp *~ .DS_Store bazel-*
特に問題なければ、bazel-*
のようにワイルドカードでexcludeしちゃって問題無いでしょう。
Bazelはドキュメントもしっかりあるので、「最適化オプション使いたい!」だとか、「マクロを定義したい!」だとかあれば、参考にしてみてください。Bazelで良き、C++のビルドライフを〜!