8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

さくらインターネットAdvent Calendar 2023

Day 18

GoでWin32 APIを叩いてみたわ~

Posted at

はじめに

はじめまして。今年、新卒採用でさくらインターネットに入社した仲座と申します。普段はクラウドのバックエンドの開発などを主に行っています。
と、自己紹介をしましたが今回は仕事の話やさくらのサービスの話ではなく、趣味開発で得た知見について語ろうと思います。よろしくお願いいたします:bow:

動機

まずはGoでWin32 APIを叩きたいと思った動機を「なぜGoで開発したいか」と「なぜWin32 APIを叩きたいのか」で大きく分けて整理したいと思います。

  • なぜGoで開発したいのか
    • 今後仕事でGoに触れる事がありそうだから
    • 言語仕様が良さげだと聞いたから
    • 環境構築が楽そうだから
  • なぜWin32 APIを叩きたいのか
    • オーディオ周りの不便さを改善したいから

なぜGoで開発したいのか

今後仕事でGoに触れる事がありそうだから

普段は前述したとおりサービス開発を行っています。そうした中、チーム内でGoを使ったモジュールの開発、社内の交流会などでGoのトピックが上がるなど注目度が高いと感じました。
「今は触れてないけど業務で触れる事が出てくるかもなぁ~」と推測、「ちょっと触っておいて、ほんとに必要な時のためにGoへの心理的なハードルを下げておくか」というようなちょっと軽いノリです。

言語仕様が良さげだと聞いたから

「Goは言語仕様が薄く、表現力に乏しい」とチラホラ聞こえてきます。表現力に乏しいというのは一見良くないように捉えることができるのですが(少なくとも私はそう思った)、この特徴は保守性を高めることに繋がるのではと考えました。
業務でPHPに触れる機会もあるのですが、私には以下のようなコードを見つけたとき1度その行を読み飛ばしてしまう癖があります。

$logs = explode("\n", $logs_str);
$changed_msg_errors = array();
foreach($logs as $log) array_push($fatal_errs, is_numeric(explode(',', $logs)[0]) && ((int)explode(',', $logs)[0] >=3 ? "Fatal"+log : log);

そもそもGoにはforeachがありませんし、forもとしてでなくとして定義しているため1行で記述する術はありません。また、三項演算子も無いため愚直にif文記述しなければならないわけです。

logs := strings.Fields(logs_str)
changed_msg_errors := []string{}
for _, v := range logs {
	level, _ := strconv.Atoi(strings.Split(v, ",")[0])
	if level >= 3 {
		changed_msg_errors = append(changed_msg_errors, "Fatal,"+v)
		continue
	}
	changed_msg_errors = append(changed_msg_errors, v)
}

これでコードゴルフ的なソースコードは少なくなるはずです。意識しなければ他の言語ほど可読性が悪くなることはなさそう...というのがGoを学ぶモチベの1つになりました。

環境構築が楽そうだから

非常にうれしいのがVSCodeの拡張機能「Go」をインストールするだけでLanguage Serverのセットアップも終わるため楽であることです。また、「後方互換性を保っている」とチラホラ聞こえるため開発環境を構築する上で最新バージョンをインストールしておけばおkというお手軽さ。複数のバージョンを切り替えて使う必要が無いので非常に楽そうだという理由です。

それに、「環境構築が楽そう」というのは実行環境においても同じことが言えます。Goはビルドするとシングルバイナリとして吐き出され、ランタイムも必要ないというのが大きい特徴な気がします。

なぜWin32 APIを叩きたいのか

オーディオ周りの不便さを改善したいから

「なぜWin32 APIを叩きたいのか」は何を作りたいのか、に言い換えることもできます。ですので何を作りたいのかを書いていきます。

ゲームをする際、普段は音量の調節にWindowsの標準ミキサー「SndVol.exe」を使っています。しかし、カーソルが吸われるタイプのフルスクリーンゲームをすると手軽に音量を調節することができません。
一度デスクトップに戻り、SndVol.exeのウィンドウにアクセスし音量を調節するわけです。FPS等、相手の足音を頼りに判断をするゲームだと一時的に音量を上げたいなんて事があると思います。Tarkovとか...
その解決策として、物理的なデバイスや卓上に置いてあるスマートフォンからWindowsのサウンドミキサにアクセスできるようにするのはどうだろうと考えたわけです。

図にするほどでもないですが、こんな感じです。

名称未設定ファイル.drawio.png

余談ですがM5から出たM5Dialというマイコンモジュール非常に気になっております。このマイコンモジュールからHTTPで自作コントローラに接続できればとてもいいミキサになると思うんです。

ただ、M5Dialのみ対応させても面白くないので自作コントローラにはHTTPで受けてもらって、タブレット端末やスマートフォンに対応させて便利に使えるようにできれば便利だし、他のアプリケーションからも連携しやすいなと。(いづれOSSとして公開したいな...)

まぁ、以上が私が作りたいものです。

実際の叩き心地

ここからはGoからWin32 APIを叩く際のコードと叩き心地について書いていこうと思います。単刀直入に申し上げますと...あんまり良くないです
圧倒的に情報が足りないのですよ...それに加え、Windows Core Audio APIをラップしているライブラリが少ないので(GoでWin32APIを叩くなんて人多くないもんね..)非常に骨が折れます。C++で言えばヘッダファイルを自前で書かなければならないわけですからね。

しかし、ライブラリが少ないとは言えありました。wcaというWindows Core Audio APIをラップしたライブラリです。
これを使えばとりあえずAudio Sessionを作りだしてるアプリケーションたちのプロセスIDを取得することまでこぎ着けました。

ソースコードはこちらで見ることができます。整理されてなく、人に見せるようなものでもありませんが興味ありましたらご覧ください。:innocent:
https://github.com/fishaction/go-audio

解決しなければならない課題

ただ、叩き心地が悪い程度ならラップするなりしていい感じにすることができるはずです。(とは言え私の技量が足りなければできませんが...)

しかし、GoでWin32 APIを叩く上で解決しなければならない課題を解決するか、解決したことにしないといけない事が出てきました。それが、現在の私の知識ではプロセスIDからアプリケーションのアイコンを取得する方法についてGoだけじゃ難しそうという事です。
↓のSndVol.exeのようにアイコンを取ってきてHTTPでクライアントに渡してあげることが難しいとは...!

image.png

func main() {
	messageBox := windows.NewLazyDLL("user32.dll").NewProc("MessageBoxW")
	if messageBox.Find() != nil {
		log.Fatalln(messageBox.Find())
	}

	var hWnd HWND = 0
	var lpText LPCTSTR = "hogehoge"
	var lpCaption LPCTSTR = "fugafuga"
	var uType MBType = MBTypeOK

	messageBox.Call(
		hWnd.uintptr(),
		lpText.uintptr(),
		lpCaption.uintptr(),
		uType.uintptr())
}

tsutigayaさんのGoからWindowsのAPIを叩くを一部抜粋して書いています。

このようにdllが存在する場合、自前で構造体を宣言し叩くことができます。しかし、GDI+で定義されているクラスのメソッドを叩く方法が私にはわからないのです

Image::Save(constWCHAR*,constCLSID*,constEncoderParameters*) メソッド
このようなやつ

結局現状はC++でexeファイルのパスからアイコンを抽出し指定したディレクトリにbmpファイルを生成する関数を定義したDLLを自作して解決したことにして(忘れようとして)います。

しかし、今まで

GOOS=windows GOARCH=amd64 go build

とだけ実行してればよかったものを、C++のビルドも考慮しなくてはならずGoのうま味が若干薄れてしまうのでは...と思ってしまうわけです。(かといってC++やC#でjsonを返すAPIも定義するか...?)

まぁそれらの理由で最適解を探しているというのが現状です。

まとめ

Goそのものは僕にとって素晴らしい言語だと感じています。システムコールなどを気にするようなものでなければ、クロスプラットフォームな開発も手軽にできる素晴らしい言語だと思いました。
サーバで実行したいスクリプト等をGoで置き換えるなどはすごく便利だろうなという印象を持ちます。
しかし、今回の「GoでWin32 APIを叩いてみたわ~」という試みは、Windows向けかつコアな事をするのは少々ハードルが高いという印象がついて終わりました。

正月のゆっくりできる内にこの問題を考えておくことにします...
ここまで読んでいただきありがとうございます。皆さん良いお年をお過ごしください!それでは~

8
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?