순수 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 방식의 기여를 받음
댓글과 토론
Hacker News 의견들
-
Win32 GUI 프로그래밍에는 묘하게 마음에 드는 점이 있음
조금 특이하긴 하지만 Raymond Chen의 블로그를 읽어보면 이유가 보임
Win32 API는 8088 프로세서 시절에 뿌리가 있어서, 특정 방식으로 하면 코드 40바이트를 아끼거나 레지스터 하나를 덜 쓰는 식의 배경이 있음
예전에 mingw와 Petzold 책으로 장난감 GUI 앱을 많이 만들었고, 커스텀 컨트롤 작성, 그래픽과 텍스트 그리기, 스크롤 처리, 히트 테스트 같은 게 전부 꽤 재미있었음
앱에서strcpy,sprintf를 쓰는 게 보이는데, 진지한 프로그래밍이라면 길이 검사를 하는 변형을 써야 함. 컴파일러가 경고를 쏟아내지 않은 게 놀라움
Win32 API에는 C 표준 라이브러리에 있는 것들을 대체하는 함수가 많음. 실행 파일 크기를 정말 줄이고 싶다면 C 표준 라이브러리 없이 앱을 작성해볼 수 있음.memset()대신ZeroMemory(),memcpy()대신CopyMemory()가 있음
어느 순간부터 순수 C 코드를 직접 쓰는 건 고통스러워지지만, 처음 몇 번은 순수 C로 해보는 게 배우는 데 가장 좋다고 봄. 세부 사항을 직접 관리하면 배우는 동안 내부에서 무슨 일이 일어나는지 감이 잘 잡힘
Win32 GUI 프로그래밍을 더 해보고 싶다면 WTL(Windows Template Library) 을 보는 걸 권함. Win32 API를 감싼 C++ 래퍼라서 동작을 훨씬 이해하기 쉬워짐- AlomWare Toolbox라는 훌륭한 PC 앱을 쓰는데, Win32 설계의 전형에 가까움(https://www.alomware.com/images/tab-automation.png)
기능이 매우 많은데도 그 덕분에 크기가 약 3MB뿐임. 프레임워크도 없고 단일 실행 파일 하나뿐이라, 모든 소프트웨어가 아직도 이런 식이면 좋겠음 - 요즘 최소한
strcpy대신strncpy를 쓰지 않으면, 사람은 물론 AI 도구까지 계속 그러면 안 된다고 말하는 걸 감수해야 함
개인적으로 Zig를 쓰는 주요 이유 중 하나가 이런 흔한 함정을 많이 줄여준다는 점임. 물론 C도 괜찮음 memset()대신ZeroMemory(),memcpy()대신CopyMemory()가 있다고 했지만, MSVC intrinsic은rep stos/movs명령을 쓸 것 같음
함수 호출보다 더 작을 수 있음. 함수 호출에는 가져오기 테이블 항목 크기도 포함되기 때문임- 그런 작업을 많이 했고, 솔직히 네이티브 코드로 네이티브 UI를 개발할 수 있던 능력이 그리움
fopen/fread/fclose도 쓰고 있는데,CreateFile/ReadFile/WriteFile같은 Win32 API를 쓰지는 않는 듯함
- AlomWare Toolbox라는 훌륭한 PC 앱을 쓰는데, Win32 설계의 전형에 가까움(https://www.alomware.com/images/tab-automation.png)
-
컨트롤마다 힘들게
CreateWindow()를 호출하는 대신, 전통적으로는.rc파일에 대화상자 리소스를 배치하고(Visual Studio에는 아직 이를 시각적으로 하는 대화상자 편집기가 있음)CreateWindow()대신CreateDialog()를 썼음
이렇게 하면 모든 컨트롤을 자동으로 만들어줌. 애플리케이션 매니페스트를 추가하면 현대적인 UI 스타일과 고DPI 지원도 얻을 수 있음- 이 방식이면 컨트롤 사이 Tab 이동과 몇 가지 키보드 단축키도 자동으로 얻음
다만 크기 조절을 원하면 여전히 수동으로 처리해야 하지만, 보통 쉽고 코드도 몇백 바이트를 넘지 않음 - C 표준 라이브러리와 운영체제 라이브러리가 겹치는 건 사실상 UNIX뿐이고, Windows 1.0이 나온 1985년에는 표준이라고 부를 만한 것도 아직 없었음
K&R C는 있었지만, UNIX 밖의 각 운영체제는 제공할 기능을 제각각 골라 썼음
게다가 UNIX 계열이 아닌 곳에서는 표준 라이브러리를 플랫폼이 아니라 C 컴파일러가 책임지는 전통이 있었음
그래서 C 라이브러리는 Borland, Watcom, Symantec, Microsoft, Green Hills, Zortech 등이 제공했음
Mac OS도 MPW가 나오기 전까지는 마찬가지였고, IBM과 Unisys, 마이크로컴퓨터와 메인프레임, OpenVMS 이전의 VMS도 그랬음
Windows 10부터는 Universal C Runtime도 있음 - 이 접근은 괜찮은 외부 함수 인터페이스(FFI) 가 있는 언어로 쉽게 옮길 수 있고, 그 외에는 아무것도 필요 없음
리소스 컴파일러도, 리소스 DLL을 만들 링커도 필요 없음
리소스 파일과 그 바이너리 형식은 좋은 API가 아님
적당히 고수준 언어에서CreateWindow호출을 갖고 있다면, 언어 안에 잘 녹아드는 리소스 비슷한 DSL을 메타프로그래밍으로 만들 수 있을 것 같음
- 이 방식이면 컨트롤 사이 Tab 이동과 몇 가지 키보드 단축키도 자동으로 얻음
-
예전에 Linux에서 어셈블리로 비슷한 걸 2KiB 미만으로 만든 적이 있음: https://gaultier.github.io/blog/x11_x64.html
다른 사람들이 말했듯 순수 C로 만들고 동적 링크를 쓰면, 적어도 Linux에서는 20KiB 아래로 쉽게 유지할 수 있음. Windows는 운영체제에 기본 포함된 것이 훨씬 많으니 더 쉬울 수도 있음
어쨌든 시도 자체는 응원함. 내 글 끝에 적어둔 링크 옵션을 시도해보면 크기를 줄이는 데 도움이 될 것임- 조금 확장한 ncurses 기반 TUI TODO 프로그램이 15K임. Linux이고 정적 링크는 아님
아직 musl로 ncurses를 빌드해보지는 못했음
- 조금 확장한 ncurses 기반 TUI TODO 프로그램이 15K임. Linux이고 정적 링크는 아님
-
“네이티브 Windows 룩앤필”이라고 해서, 실제 실행 전에는 목록 뷰에 컨텍스트 메뉴가 있고 특정 필드를 더블클릭하면 인라인에 가까운 드롭다운 메뉴나 텍스트 박스가 뜨길 기대했음
이제는 Win32 UI/UX에 맞게 프로그램을 설계하는 법을 모르는 건지, 아니면 내가 너무 늙은 건지 모르겠음 -
“프레임워크 없음”이라더니, DPI 스케일링에서 글꼴이 흐릿하고, Tab 지원도 없고, 텍스트 필드에서 Ctrl-A로 전체 선택도 안 되며, 현대 이전 프레임워크도 제공하던 여러 기능이 빠져 있고, 행 추가 시 오류도 남
“현대적”이라는 건 어떤 의미인지 모르겠음- DPI 인식 설정 예시는 여기 있음: https://github.com/Dwedit/GameStretcher/blob/master/Stretche...
이 코드는user32:SetProcessDpiAwarenessContext,shcore:SetProcessDpiAwareness,user32:SetProcessDPIAware중 사용 가능한 것을 동적으로 확인해 호출함
Windows 버전이 아주 오래되어 이 중 아무것도 구현하지 않았다면(Windows XP 이전), 아무것도 호출하지 않음 - “현대적”이라는 건 필요한 것보다 훨씬 크면서 기능은 많이 빠져 있다는 뜻인 듯함
빠졌다고 언급한 기능 대부분은 특히 컨트롤 사이 Tab 이동 같은 경우 추가하기 매우 쉬움
- DPI 인식 설정 예시는 여기 있음: https://github.com/Dwedit/GameStretcher/blob/master/Stretche...
-
내 안의 6502 프로그래머는 278KB가 이제 가볍다고 통하는 걸 보며 속으로 죽어가고 있음
- 이 바이너리를 재현해서 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=> 100KB
gcc -Oz=> 99KB
gcc -flto=> 101KB
gcc -s=> 51KB
gcc -s -Oz -flto=> 47KB
작은.exe크기만 관심사라면 아직 개선 여지가 많음 - 상당 부분은 플랫폼과 실행 파일 형식 때문임
스택 추적 정보, 동적 링크 인프라, 예외 처리 테이블 같은 게 없으면 훨씬 더 가벼울 수 있음. 예외 처리 테이블은 예외가 C 함수를 가로질러 지나갈 수 있어서 C에서도 필요함 - 데모신 대회에 64KB TODO 앱 부문을 만들어달라고 청원해볼 수도 있겠음
- 솔직히 그렇게 큰 게 놀라움. 더 작거나, 절반쯤은 앱 아이콘이 차지할 줄 알았음
예전에 이런 걸 작성했을 때는 더 작았던 기억이 있음. 혹시 MinGW 때문일까? - 갑자기 Win32를 어셈블리로 프로그래밍하는 게 꽤 힙해졌던 시절이 떠오름
아마 쉐어웨어 다운로드 크기가 커지던 흐름에 대한 반응이었을 것임. MFC의 암흑기였음
초기 Palm Pilot 68k 프로그래밍과 함께, 레트로컴퓨팅이 아닌 어셈블리의 마지막 환호로 기억함
- 이 바이너리를 재현해서 278KB가 어디에 쓰이는지 보려 했음
-
친구들, 그냥 한번 해보고 재미로 만든 앱임. 댓글들이 맞고, 이런 건 C++나 다른 언어로 더 합리적으로 만들 수 있었을 것 같음
- 약 30년 전 처음 Windows 프로그램을 만들 때 나도 정확히 이런 식으로 배웠음. 다만 C++ 컴파일러를 썼을 것임
이유는 잘 모르겠지만, Windows API 문서가 C 스타일 코드를 C++ 컴파일러로 쓰는 방식으로 안내했던 것 같음. Microsoft가 C++를 C의 개선된 상위 집합으로 보고, C 스타일 코드에도 쓰는 게 맞다고 여겼던 듯함 - 농담이 아니라 기본 Windows 11 TODO 앱보다 이 앱을 쓰고 싶음
- 이게 딱 그래야 하는 방식임
Win32 API를 쓰려면 다른 언어가 근본적으로 바꾸는 건 없고, 오히려 더 헷갈리게 만들 수 있음
사람들은 C++식 사고에 자주 빠지고, Win32 API에 익숙하지 않은 사람에게는 전체를 더 혼란스러운 덩어리로 만들었을 것임
아주 귀여운 시도이고, 다른 사람들이 어떻게 생각하든 Win32 API에 대한 익숙함은 괜찮은 기본 역량임 - https://github.com/Efeckc17/YoutubeGO 참고로 이 애플리케이션도 리뷰하거나 살펴봐주면 정말 기쁠 것 같음
- 이런 깔끔한 네이티브 UI 프로젝트가 내가 프로그래밍을 배우게 만든 동기였음. 멋짐
- 약 30년 전 처음 Windows 프로그램을 만들 때 나도 정확히 이런 식으로 배웠음. 다만 C++ 컴파일러를 썼을 것임
-
여기서 사용자가 마우스를 움직일 때마다 278KB 원격 측정을 보내려고 수 MB의 JS나 C#을 로드하는 소프트웨어나 웹사이트를 만드는 사람들이 꽤 빈정대는 게 보임
- C# + WinForms로 만든 비슷한 앱은 디스크에서 10KB 미만이고 RAM은 6MB를 씀
이 앱은 RAM 1.5MB를 쓰며, 둘 다 즉시 시작됨
- C# + WinForms로 만든 비슷한 앱은 디스크에서 10KB 미만이고 RAM은 6MB를 씀
-
Windows 앱을 개발할 때 모든 걸 C로 할 수 있다는 사실은 늘 기분 좋게 만듦
가장 낮은 수준의 기본 요소부터 쌓아 올리는 방식이 그냥 자연스럽게 느껴짐
반면 macOS에서는 모든 것이 Objective-C 객체라서, 순수 C로 앱을 쓸 수는 있지만 Apple이 의도한 추상화보다 한 단계 아래로 내려간 셈이라 거의 1000배 더 장황해짐. 무언가를 하려면 사실상 Objective-C 클래스 계층을 꼭두각시처럼 조종해야 해서 정말 찝찝함
왜 Objective-C의 클래스 기반 API를 기본적인 Win32식 절차형 API 위에 다시 얹을 수 없는지 모르겠음. 기술적으로 Win32도 “클래스 기반”이긴 하지만 최소한임
그래서 당분간 내 C 코드를 macOS로 포팅할 생각이 잘 들지 않음- 흥미롭게도 Windows에는 COM, 그리고 COM 위에 만든 WinRT라는 자체 객체 지향 API/ABI가 있음
하지만 COM 인터페이스용 MIDL 컴파일러는 C에서 인터페이스를 사용하고 구현할 수 있는 헤더를 만들어줌: https://www.codeproject.com/KB/com/com_in_c1.aspx
내가 알기로는 WinRT에서도 오늘날 여전히 동작함. 다만 생성된 C 구조체와 함수 이름이 전체 네임스페이스를 포함해야 해서 매우 못생겨짐: https://stackoverflow.com/a/7437006/111335 - 절차형 API가 있긴 했고, 이름은 Carbon이었음
user32.dll이 노골적으로 베낀 Macintosh Toolbox API의 거의 드롭인 대체품이었음
다만 프로그래머가 느끼는 따뜻함을 제외하면, UI는 정말로 객체 지향을 원함. 공통 동작을 일부 공유하는 서로 다른 객체들의 트리는 UI 코드의 99%를 거의 그대로 설명함
Toolbox와 Carbon은 그런 객체 지향이 부족해서 꽤 무리했음. Windows가 실제로 더한 독창적인 생각 하나가 바로 윈도우 클래스였음
개인적으로는 C에서 Objective-C 클래스를 직접 다룰 때 느끼는 이상함을, 나는user32.dll코드에서 윈도우 클래스나 윈도우 프로시저를 정의해야 할 때마다 느낌[0]
UI가 객체 지향을 원하듯, 객체 지향은 전용 언어 기능을 원함. 없어도 만들 수는 있지만 사용성은 2000배 나빠짐
[0] 또는 GTK/GObject와 관련된 모든 것
- 흥미롭게도 Windows에는 COM, 그리고 COM 위에 만든 WinRT라는 자체 객체 지향 API/ABI가 있음
-
정적 라이브러리에 링크하고 있는 것처럼 보임
정적 라이브러리가 아니라 DLL에 링크해야 하며, 그러면 애플리케이션 크기가 크게 줄어듦- 그건 반대로 보임
어차피 DLL을 프로그램과 함께 배포해야 한다면(운영체제의 일부가 아니므로), DLL에는 전체 기능이 들어 있고 각 DLL이 자체 C 런타임까지 가질 수도 있음
모든 것을 단일 EXE에 정적으로 컴파일하면 C 런타임은 하나만 있고, 사용하지 않는 함수도 쉽게 제거할 수 있음
DLL은 그 코드가 여러 프로그램 사이에서 공유될 때만 크기를 줄여줌 - 아니, CRT의 정적 버전에 링크하는 건 좋은 선택임. 쓰지 않는 코드를 잘라낼 수 있음
MSVCRxx/VCRUNTIME에 동적 링크하면 사용자가 Microsoft에서 정확히 그 DLL을 다운로드해야 함
MSVCRT에 동적 링크하면 그 문제는 없지만, Visual Studio에서는 하기가 매우 어려움
CRT에 정적 링크하면 정말 안 되는 경우는 LGPL 준수 정도일 듯함
- 그건 반대로 보임