GUI-приложение размером менее 1 Кб

в 8:35, , рубрики: Crinkler, WinAPI, ненормальное программирование

На досуге задался вопросом возможности создания приложения, со следующими требованиями:

  • хоть сколько-нибудь полезная прикладная функция (то есть не пустышка)
  • наличие оконного интерфейса
  • размер менее 1 Кб


Вообще, эталон приложения с размером до 1 Кб — это 1k intro, являющееся разновидностью демосцен. Чаще всего это написанная на ассемблере инициализация OpenGL с последующим скармливанием ему шейдера, который и выполняет основную работу (отрисовывает какие-нибудь раскрашенные фракталы). Плюс всё это пожато упаковщиком (например crinkler)
Эти приложения в буквальном смысле вылизаны до байта, их разработка занимает недели и даже месяцы.

Такой подход слишком суров, я решил не выходить за рамки обычного прикладного программирования на WinAPI.

В это время в Steam началась летняя распродажа, где взрослым людям предлагалось дичайше затыкивать инопланетян мышой в браузере (да-да, Valve снова делает игры, как и обещал Гейб).
Это оскорбило меня до глубины души, и я решил попробовать реализовать простейший автокликер с минимальными настройками.

Требовалось уместить в 1 Кб:

  • создание и инициализацию интерфейса
  • оконную функцию с обработчиками событий
  • основную логику приложения (построенную на функциях GetAsyncKeyState и SendInput)

Приложение будет создаваться в MSVC на чистом C без ассемблера и СМС, а затем сжиматься пакером crinkler. Следует отметить, что данные (особенно разреженные) crinkler сжимает гораздо эффективнее, чем код (раза эдак в два). Поэтому будем стараться максимум функционала переносить внутрь данных.

Начав с классических CreateWindow для каждоко элемента окна, я понял, что в требуемый размер я никак не влезу.

Пришлось искать альтернативу. И ей стала функция CreateDialogIndirect, создающая диалог из заполненной структуры DLGTEMPLATE (состоящей из кучи DLGITEMTEMPLATE)

Для удобного создания и заполнения структуры я завёл немножко макросов вроде таких:

#define NUMCHARS(p) (sizeof(p)/sizeof((p)[0]))

#define DLGCTL(a) struct{DWORD style; DWORD exStyle; short x; short y; short cx;
short cy; WORD id; WORD sysClass; WORD idClass; WCHAR wszTitle[NUMCHARS(a)]; WORD cbCreationData;}

#define RADIO(x,y,cx,cy,id,title) {WS_VISIBLE|WS_CHILD|BS_RADIOBUTTON, 0, (x)/2, (y)/2,n
(cx)/2,(cy)/2, id, 0xFFFF, 0x0080, title, 0}

Теперь можно объявить и заполнить структуру элементами будущего окна:

static struct
{
	DWORD style; DWORD dwExtendedStyle; WORD ccontrols; short x; short y; short cx;  short cy; WORD menu; WORD windowClass;

	DLGCTL(LBL_BTN_LEFT)	button_left;
	DLGCTL(LBL_BTN_MIDDLE)	button_middle;
	DLGCTL(LBL_BTN_RIGHT)	button_right;
} Dlg = 
{
	WS_VISIBLE|WS_POPUP|WS_BORDER, 0, TOTAL_CONTROLS, 50/2, 50/2, WND_CX/2, WND_CY/2, 0, 0,

	RADIO(10,  0, 80, 30, IDC_BTN_LEFT,   LBL_BTN_LEFT),
	RADIO(100, 0, 80, 30, IDC_BTN_MIDDLE, LBL_BTN_MIDDLE),
	RADIO(190, 0, 68, 30, IDC_BTN_RIGHT,  LBL_BTN_RIGHT),
};

Скармливаем структуру функции CreateDialogIndirect, и вот получившееся окно:

GUI-приложение размером менее 1 Кб - 1

Так как мы умещаемся в 1 кб, то манифеста, как и всего прочего, у нас нет, а значит и визуальных стилей тоже. Всё серое и квадратное, как в молодости.

Но мы всё-таки извернёмся, дёрнув манифест из shell32.dll и применив контекст на его основе к нашему приложению:

static ACTCTX actCtx = {sizeof(ACTCTX), ACTCTX_FLAG_RESOURCE_NAME_VALID|ACTCTX_FLAG_SET_PROCESS_DEFAULT|ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, "shell32.dll", 0, 0, tmp, (LPCTSTR)124, 0, 0};

	GetSystemDirectory(tmp,sizeof(tmp));
	LoadLibrary("shell32.dll");
	ActivateActCtx(CreateActCtx(&actCtx),(ULONG_PTR*)&tmp);

Вот уже стильно, модно:

GUI-приложение размером менее 1 Кб - 2

Оконную функцию удалось ужать до довольно компактной:


switch(msg)
{
  case WM_COMMAND:
    switch(LOWORD(wParam))
    {
       case IDC_BTN_LEFT:
       case IDC_BTN_MIDDLE:
       case IDC_BTN_RIGHT:
         input[0].mi.dwFlags = wParam;
         input[1].mi.dwFlags = (wParam<<1);
         CheckRadioButton(hWnd,IDC_BTN_LEFT,IDC_BTN_MIDDLE,wParam);
         break;

       case IDC_BTN_HOLD:
       case IDC_BTN_TRIGGER:
         trigger_mode = (wParam==IDC_BTN_TRIGGER);
         CheckRadioButton(hWnd,IDC_BTN_HOLD,IDC_BTN_TRIGGER,wParam);
         break;

       case IDC_EDIT_PERIOD:
         period = GetDlgItemInt(hWnd,IDC_EDIT_PERIOD,(BOOL*)&tmp[0],0);
         break;

       case IDC_BTN_EXIT:
         exit(0);
    }
    break;
  }
    
  return DefWindowProc(hWnd,msg,wParam,lParam);

И тут я подумал, что хорошо бы добавить обработку аргументов командной строки, дабы пользователь имел возможность запускаться с нужными настройками.

Например эдак:

input.exe /L /T /P:20 /K:9 - кликать левой кнопкой мыши каждые 20 мсек, режим включается/выключается клавишей Tab

Перенесём часть функционала внутрь данных и получим что-то вроде:

static unsigned int arg_to_cmd[] = {IDC_BTN_HOLD, 0, 0, IDC_EDIT_KEY, IDC_BTN_LEFT, IDC_BTN_MIDDLE, 0, 0, IDC_EDIT_PERIOD, 0, IDC_BTN_RIGHT, 0, IDC_BTN_TRIGGER};

i = (char*)GetCommandLine();
while(*i)
{
  if (*(i++)=='/')//looking for argument
    switch(*i)
    {
      case 'L':
      case 'M':
      case 'R':
      case 'H':
      case 'T':
        SendMessage(hWnd,WM_COMMAND,arg_to_cmd[*i-'H'],0);//send button command
        break;
      case 'P':
      case 'K':
        t = atoi(i+2);
        SetDlgItemInt(hWnd,arg_to_cmd[*i-'H'],t,0);
        if(*i=='P')
          period = t;
        else
          key = t;
        break;
    }
  }

Обработчик вышел весьма небольшим. Естественно, никаких проверок и защит от неверного ввода, только необходимый минимум.

Теперь основной функционал (его я вынес в отдельный поток):

while(1)
{
  key_state = (GetAsyncKeyState(key)>>1);

  if (trigger_mode)
  {
    if ((key_state)&&(key_state!=prev_key_state))
      active^= 1;
    prev_key_state = key_state;
  }
  else
    active = key_state;

  if (active)
    SendInput(2,(LPINPUT)&input,sizeof(INPUT));
  Sleep(period);
}

Жмём получившийся obj-файл crinkler'ом — на выходе 973 байта.

Из них данные занимают 163 байта (степень сжатия 33.1%), код — 517 байт (степень сжатия 68.9%).

Можно ужать и сильнее (ещё байт на 50), но цель и так достигнута. Даже остался 51 запасной байт.

К первоначальнми требованиям по ходу добавились:

  • обработка аргументов командной строки
  • отображение окна с визуальными стилями

Местами код выглядят весьма криво. Это потому, что я его криво написал. А ещё кое-где это позволило сэкономить место.

Наверняка можно придумать ещё пару-тройку способов уменьшить размер приложения, но не кардинально (я думаю, байт на 50).

Результат:

Теперь можно закликивать инопланетян в автоматическом режиме с диким уроном в секунду.

Вполне возможно создавать сверхкомпактные приложения с реально используемым полезным функционалом и оконным интерфейсом.

Новизна:
Нулевая. Собрал в кучу несколько приёмовнаработок.

Целесообразность:
Бесполезно, забавы для.

Исходник
Бинарник

Принимаются критика, пожелания, предложения, восхищения, возмущения.

Автор: Boroda1

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js