純粋なCでネイティブWindows Todoアプリを作成(278KB、フレームワーク不使用)
(github.com/Efeckc17)- Simple Todoは、CとWin32 APIで作られたネイティブWindows Todoアプリで、Windows GUIプログラミングとシステム統合機能を示すプロジェクト
- 主な機能は、Todoの作成・編集・削除、完了表示、システムトレイへの最小化、Windows自動起動オプション、大文字小文字を区別しないリアルタイム検索
- データは
%APPDATA%\TodoApp\todos.datにバイナリファイルとして保存され、Todo一覧全体を保持し、最大100件のTodoをサポート - ビルドにはWindowsでMinGW-w64とWindows SDKが必要で、リポジトリをクローンした後に
.\build.batでコンパイル - 構成は
main.c、todo.c、gui.c、utils/search.cに分かれており、GUIはWin32 API・Common Controls・UXThemeを使用
プロジェクトの性格
- Simple Todoは、CとWin32 APIで書かれたネイティブWindows Todoアプリケーション
- 軽量なネイティブ実行ファイルを目指しており、Windowsの標準的なルック&フィールと最新のWindowsビジュアルスタイルを使用
- GUIはWin32 APIベースで、システムトレイ統合とファイル入出力によるデータ永続化を含む
Todo機能
- Todo項目を作成、編集、削除できる
- タスクを完了状態として表示できる
- 各Todoに優先度を設定できる
- 検索機能はリアルタイムで動作し、大文字小文字を区別しないマッチングをサポート
- アプリをシステムトレイに最小化できる
- Windows起動時に自動実行するオプションを提供
データ保存方式
- Todoデータは
%APPDATA%\TodoApp\todos.datに保存される - 保存形式は、Todo一覧全体を保持するバイナリファイル
- 保存容量は最大100件のTodoに制限される
ビルドと実行
- 要件
- Windows OS
- MinGW-w64 GCCコンパイラ
- MinGWに含まれるWindows SDK
- ビルド手順
- MinGW-w64をインストールし、
binディレクトリをシステムPATHに追加 - リポジトリをクローンした後、
simple-todo-cディレクトリに移動 .\build.batを実行してビルド
- MinGW-w64をインストールし、
- 実行は
bin/todo.exeで開始
実装構成
- プロジェクト構造は機能別のCファイルに分かれる
src/main.c: アプリケーションのエントリポイントsrc/todo.c,src/todo.h: Todo管理ロジックとデータ構造src/gui.c: GUI実装src/utils/search.c,src/utils/search.h: 検索機能の実装と宣言src/app.manifest: Windowsビジュアルスタイル用のマニフェスト
- 中核構成要素はWin32 API、Common Controls、UXTheme、File I/O
- デバッグについては、Visual StudioやVS Codeなどで利用できる
DEBUGGING.mdガイドを提供
ライセンスと貢献
- ライセンスはMIT License
- Pull Request形式での貢献を受け付ける
3件のコメント
ロマンがありますね
Hacker News のコメント
Win32 GUI プログラミングには、どこか不思議と気に入ってしまうところがある
少し変わってはいるが、Raymond Chen のブログを読むと理由が見えてくる
Win32 API は 8088 プロセッサの時代にルーツがあり、特定のやり方をするとコードを 40 バイト節約できたり、レジスタを 1 つ少なく使えたりする、といった背景がある
昔 mingw と Petzold の本でおもちゃの GUI アプリをたくさん作ったが、カスタムコントロールの作成、グラフィックやテキストの描画、スクロール処理、ヒットテストなどはどれもかなり楽しかった
アプリ内で
strcpy、sprintfを使っているのが見えるが、真面目なプログラミングなら長さチェック付きの派生版を使うべきだ。コンパイラが警告を山ほど出していないのが不思議なくらいだWin32 API には C 標準ライブラリにあるものを置き換える関数が多い。実行ファイルサイズを本当に削りたいなら、C 標準ライブラリなしでアプリを書くこともできる。
memset()の代わりにZeroMemory()、memcpy()の代わりにCopyMemory()があるある時点から純粋な C コードを直接書くのはつらくなるが、最初の数回は純粋な C でやってみるのが学習には一番だと思う。細部を自分で管理すると、学んでいる間に内部で何が起きているのか感覚をつかみやすい
Win32 GUI プログラミングをもっとやってみたいなら、WTL(Windows Template Library) を見ることを勧める。Win32 API を包んだ C++ ラッパーなので、動作をはるかに理解しやすくなる
機能が非常に多いのに、そのおかげでサイズは約 3MB しかない。フレームワークもなく、単一の実行ファイル 1 つだけなので、すべてのソフトウェアが今でもこんなふうならいいのにと思う
strcpyの代わりにstrncpyを使わないと、人間はもちろん AI ツールにまで、そんなことを続けるなと言われるのを覚悟しなければならない個人的に Zig を使う主な理由の 1 つは、こうしたよくある落とし穴をかなり減らしてくれる点だ。もちろん C も悪くない
memset()の代わりにZeroMemory()、memcpy()の代わりにCopyMemory()があると言ったが、MSVC intrinsic はrep stos/movs命令を使いそうだ関数呼び出しより小さくなる可能性がある。関数呼び出しにはインポートテーブル項目のサイズも含まれるためだ
fopen/fread/fcloseも使っているが、CreateFile/ReadFile/WriteFileのような Win32 API は使っていないようだコントロールごとに苦労して
CreateWindow()を呼ぶ代わりに、伝統的には.rcファイルに ダイアログリソースを配置し(Visual Studio には今でもこれを視覚的に行うダイアログエディタがある)、CreateWindow()の代わりにCreateDialog()を使っていたこうすればすべてのコントロールを自動で作ってくれる。アプリケーションマニフェストを追加すれば、モダンな UI スタイルと高 DPI 対応も得られる
ただしサイズ変更をしたい場合は依然として手動で処理する必要があるが、たいてい簡単で、コードも数百バイトを超えない
K&R C はあったが、UNIX 以外の各 OS は提供する機能をそれぞれ好きに選んでいた
さらに UNIX 系でないところでは、標準ライブラリはプラットフォームではなく C コンパイラが責任を持つという伝統があった
そのため C ライブラリは Borland、Watcom、Symantec、Microsoft、Green Hills、Zortech などが提供していた
Mac OS も MPW が出るまでは同様で、IBM や Unisys、マイクロコンピュータとメインフレーム、OpenVMS 以前の VMS もそうだった
Windows 10 以降には Universal C Runtime もある
リソースコンパイラも、リソース DLL を作るリンカも不要だ
リソースファイルとそのバイナリ形式は良い API ではない
そこそこ高水準の言語で
CreateWindow呼び出しを持っているなら、その言語になじむリソース風の DSL をメタプログラミングで作れそうだ以前 Linux で、似たようなものをアセンブリで 2KiB 未満に収めて作ったことがある: https://gaultier.github.io/blog/x11_x64.html
他の人たちが言っているように、純粋な C で作って動的リンクを使えば、少なくとも Linux では 20KiB 未満に簡単に収められる。Windows は OS に標準で含まれているものがずっと多いので、さらに簡単かもしれない
いずれにせよ、試み自体は応援したい。私の記事の最後に書いておいたリンクオプションを試すと、サイズ削減の助けになるはずだ
まだ musl で ncurses をビルドできてはいない
「ネイティブな Windows のルック&フィール」と言うので、実際に実行するまでは、リストビューに コンテキストメニューがあり、特定のフィールドをダブルクリックするとインラインに近いドロップダウンメニューやテキストボックスが出るものだと期待していた
今では Win32 UI/UX に合わせてプログラムを設計する方法を知らないのか、それとも自分が年を取りすぎただけなのか分からない
「フレームワークなし」と言っていたのに、DPIスケーリングでフォントがぼやけ、Tabサポートもなく、テキストフィールドでCtrl-Aによる全選択もできず、現代以前のフレームワークでさえ提供していた多くの機能が欠けていて、行追加時にもエラーが出る
「モダン」とはどういう意味なのか分からない
このコードは
user32:SetProcessDpiAwarenessContext、shcore:SetProcessDpiAwareness、user32:SetProcessDPIAwareのうち利用可能なものを動的に確認して呼び出すWindowsのバージョンが非常に古く、これらのどれも実装していない場合(Windows XP以前)は、何も呼び出さない
抜けていると指摘した機能の大半、特にコントロール間のTab移動などは、追加するのがとても簡単
私の中の6502プログラマーは、278KBが今では軽量と見なされるのを見て、内心死にかけている
最初にぶつかった障害は、gitが
core.autocrlf=falseに設定されているとbuild.batファイルが動作しないことだった。core.autocrlf=trueに変えて再クローンしたらビルドできたツールチェーンは https://github.com/niXman/mingw-builds-binaries/releases/tag... の
x86_64-15.1.0-release-win32-seh-msvcrt-rt_v12-rev0.7zを使ったこの場合、102KBの
.exeファイルができる。主張されている278KBより出だしからずっと良い。作者が別のツールチェーンや設定を使っている可能性もあるので、正確な再現手順があるとありがたいGCCにいくつかスイッチを渡すとさらに小さくできる
gcc -Os=> 100KBgcc -Oz=> 99KBgcc -flto=> 101KBgcc -s=> 51KBgcc -s -Oz -flto=> 47KB小さな
.exeサイズだけが関心事なら、まだ改善の余地は大きいスタックトレース情報、動的リンクのインフラ、例外処理テーブルのようなものがなければ、はるかに軽くできる。例外処理テーブルは、例外がC関数をまたいで通過する可能性があるため、Cでも必要になる
以前こういうものを書いたときは、もっと小さかった記憶がある。もしかしてMinGWのせいだろうか?
おそらく、シェアウェアのダウンロードサイズが大きくなっていく流れへの反応だったのだろう。MFCの暗黒時代だった
初期のPalm Pilot 68kプログラミングと並んで、レトロコンピューティングではないアセンブリの最後の歓声として記憶している
みんな、これはただ一度やってみて、面白半分で作ったアプリだ。コメントの指摘は正しいし、こういうものはC++や他の言語で作ったほうがもっと合理的だったと思う
理由はよく分からないが、Windows APIのドキュメントはCスタイルのコードをC++コンパイラで使うよう案内していたように思う。MicrosoftはC++をCの改良された上位集合と見なし、Cスタイルのコードにも使うのが正しいと考えていたのだろう
Win32 APIを使うなら、他の言語が根本的に変えることはなく、むしろさらに分かりにくくすることがある
人々はC++的な考え方に陥りがちで、Win32 APIに慣れていない人にとっては、全体をさらに混乱した塊にしてしまっただろう
とてもかわいい試みだし、他の人がどう思おうと、Win32 APIへの慣れは悪くない基礎スキルだ
ここでは、ユーザーがマウスを動かすたびに278KBのテレメトリを送るために数MBのJSやC#をロードするソフトウェアやWebサイトを作っている人たちが、かなり皮肉を言っているのが見える
このアプリはRAMを1.5MB使い、どちらも即座に起動する
Windows アプリを開発するとき、すべてを C でできるという事実はいつも気分を良くしてくれる
最も低レベルな基本要素から積み上げていくやり方が、ただ自然に感じられる
一方 macOS ではすべてが Objective-C オブジェクトなので、純粋な C でアプリを書くことはできるものの、Apple が意図した抽象化より一段下に降りることになり、ほとんど 1000 倍冗長になる。何かをするには、事実上 Objective-C のクラス階層を操り人形のように操作しなければならず、本当に気持ち悪い
なぜ Objective-C のクラスベース API を、基本的な Win32 風の手続き型 API の上に改めて載せられないのか分からない。技術的には Win32 も「クラスベース」ではあるが、少なくとも最小限だ
だから当面、自分の C コードを macOS に移植しようという気にはあまりならない
ただし COM インターフェース用の MIDL コンパイラは、C でインターフェースを使い実装できるヘッダーを生成してくれる: https://www.codeproject.com/KB/com/com_in_c1.aspx
自分の知る限り、WinRT でも現在なお動作する。ただし生成される C の構造体と関数名は名前空間全体を含める必要があるため、非常に不格好になる: https://stackoverflow.com/a/7437006/111335
user32.dllが露骨に真似た Macintosh Toolbox API の、ほぼドロップイン置換だったただしプログラマーが感じる温かみを除けば、UI は本当にオブジェクト指向を求める。共通の振る舞いを一部共有する異なるオブジェクトのツリーは、UI コードの 99% をほぼそのまま説明している
Toolbox と Carbon はそうしたオブジェクト指向性が不足していたため、かなり無理があった。Windows が実際に加えた独創的な考えの一つが、まさにウィンドウクラスだった
個人的には、C から Objective-C クラスを直接扱うときに感じる違和感を、
user32.dllコードでウィンドウクラスやウィンドウプロシージャを定義しなければならないたびに感じる[0]UI がオブジェクト指向を求めるように、オブジェクト指向は専用の言語機能を求める。なくても作ることはできるが、使い勝手は 2000 倍悪くなる
[0] あるいは GTK/GObject に関するすべて
静的ライブラリにリンクしているように見える
静的ライブラリではなく DLL にリンクすべきで、そうすればアプリケーションサイズは大きく減る
いずれにせよ DLL をプログラムと一緒に配布しなければならないなら(OS の一部ではないので)、DLL には全機能が入っていて、各 DLL が独自の C ランタイムまで持つ可能性もある
すべてを単一の EXE に静的コンパイルすれば、C ランタイムは一つだけで、使っていない関数も簡単に削除できる
DLL は、そのコードが複数のプログラム間で共有される場合にだけサイズを減らしてくれる
MSVCRxx/VCRUNTIMEに動的リンクすると、ユーザーが Microsoft からまさにその DLL をダウンロードする必要があるMSVCRTに動的リンクすればその問題はないが、Visual Studio では非常にやりにくいCRT に静的リンクすると本当に問題になるのは、LGPL 準拠くらいだと思う
兄貴たちの鼻息がここまで伝わってくるような感じ…