Indrekis
Indrekis

Categories

  • retrocomputing
  • ibm_pc_compat

Tags

  • retrocomputing
  • IBM PC та сумісні
  • Linux
  • X Window System

Крім підручників KDE (1, 2), переклав тоді ще підручник по Xlib. Що цікаво, він ще цілком актуальний – завдяки стабільності X Window System.

Оригінальний архів тут. Також, доступний оригінальний сайт.

Xlib програмування: короткий курс

Вступ

Я не знайшов в мережі Web ніякого задовільного курсу програмування з допомогою (для) Xlib. Більшість з них, як на мій смак, занадто Motif-орієнтовані. Крім того, я відповідаю на питання по X програмуванню майже щодня, тому почав разом збирати разом ці маленькі огляди (coursewares).

Важливі зауваження: приклади програм написані на C++, але це в основному, щоб мати можливість оголошувати змінні де завгодно1.

Почнемо з короткого оповідання: вічна історія новенького в Xlib, який/яка пише свою першу програму.

Добре, я відкрив з’єднання з X сервером (що б це не означало), за допомогою XOpenDisplay, потім створюю вікно, скориставшись XCreateWindow, потім малюю лінію функцією XDrawLine. Далі, програма спить протягом десяти секунд, щоб я міг побачити результат. Звучить просто.

Бідний новачок пише програму. І нічого не відбувається. Він кличе свого товариша чарівника.

А ти виконав XFlush після того як все зробив?
Ні, навіщо?
Запит залишається на клієнті,” – чарівник заговорюється2, думає бідний новачок, – поки ти цього не зробиш.

Бідний новачок змінює програму. І нічого не відбувається. Тоді він знову звертається до свого товариша.

Ти відобразив (map) своє вікно?
Що???
Створення вікна не змушує його з’явитися на екрані. Спершу ти повинен відобразити його за допомогою XMapWindow.

Бідний новачок змінює програму. Вікно з’являється, але в ньому нічого нема (щось таке, як тут). Тоді він знову звертається до свого товариша.

Ти зачекав на MapNotify перед тим як малювати свою лінію? – ще одна дивна фраза чарівника.
Ні, а навіщо?
X використовує безстанову модель малювання, вміст вікна може втратитись, коли вікно не на екрані. — (Це вже забагато, чому ці чарівники-спеціалісти не можуть говорити нормально, як я і ти ?) – Ти мусиш зачекати на MapNotify перед тим, як малювати.

Бідний новачок змінює програму. Все стає складніше і складніше. Не так просто, як спершу здавалося. Цикл отримує повідомлення, аж поки не прийде MapNotify. Вікно з’являється порожнім. Тоді бідний новачок звертається до свого товариша.

Як я зрозумів, ти вибрав структуру StructureNotifyMask в своєму вікні?
???
Просто зроби це, і все буде добре.

Бідний новачок виправляє програму. І стається чудо! Лінія у вікні. З цього моменту програма виглядає ось так (вона насправді трохи складніша, ніж ви могли вирішити з діалогу).

Тепер ви зрозуміли принаймі дві речі:

Як з допомогою X намалювати у вікні лінію.
Навіщо комусь може бути потрібен курс по X.

А зараз, якщо ви хочете вивчити більше і глибше зрозуміти цю програму, переходьте до наступного уроку.

Детальніше про X.

prog-1.cc

// Written by Ch. Tronche (http://tronche.lri.fr:8000/)
// Copyright by the author. This is unmaintained, no-warranty free software. 
// Please use freely. It is appreciated (but by no means mandatory) to
// acknowledge the author's contribution. Thank you.
// Started on Thu Jun 26 23:29:03 1997

//
// Xlib tutorial: перша програма
// Примушує вікно з'являтися на екрані.
//

#include <X11/Xlib.h> // Кожна Xlib програма мусить включати це
#include <assert.h>   // Я включив це, щоб тестувати повернені 
                      // значення ліниво
#include <unistd.h>   // Отже ми отримаємо профіль на 10 секунд

#define NIL (0)       //  Назва вказівника "ні на що"

main()
{
      Display *dpy = XOpenDisplay(NIL);
      assert(dpy);
      Window w = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, 
			       200, 100, 0, 
			       CopyFromParent, CopyFromParent, CopyFromParent,
			       NIL, 0);
      XMapWindow(dpy, w);
      XFlush(dpy);
      sleep(10);
}

Зауваження перекладача 1: Це тоді й нульовий вказівник доводилося самостійно означати?..

Зауваження перекладача 2: Щодо профілю – так в оригіналі, “So we got the profile for 10 seconds”. Та й NIL там дивно підписаний: “ A name for the void pointer”…

Зауваження перекладача 3: Автор не наводить, як компілювати програму. Тому я спробував самостійно, на Ubuntu LTS 24.04 та на Mandrake 7.

Компілюємо код, без змін, під Ubuntu LTS 24.04. Відсутність int перед main() компілятор дещо тривожить, але, в принципі, не стримує його. Кумедно, що програма з 90-х компілюється і запускається з-під WSL2, використовуючи Xming як X server. Якщо програму закрити, вона все рівно чекає до кінця десяти секунд. Ремарка: оригінальний текст не містить ілюстрацій.
І вона ж, під Mandrake7. Зауважте, компілятор більш капризний, зокрема, зважає на порядок опцій -L i -l – відвик я. Зверніть увагу також на назву вікна.

Анатомія базової Xlib програми

Програма починається стандартно3:

#include <X11/Xlib.h> // Кожна Xlib програма мусить включати це
#include <assert.h>   // Я включив це, щоб тестувати повернені  
                      // значення ліниво
#include <unistd.h>   // Отже ми отримаємо профіль на 10 секунд

#define NIL (0)       //  Назва вказівника "ні на що"

Далі йдуть серйозні речі. Спочатку ми відкриваємо з’єднання з сервером.

Display *dpy = XOpenDisplay(NIL);
assert(dpy);

Якщо спроба провалилася (а вона може), XOpenDisplay() поверне NIL.

Ми створимо вікно, але нам спершу потрібно отримати колір фону вікна. X використовує дуже складну систему кольорів, щоб пристосуватися до кожного можливого “шматка” апаратури. Кожен колір кодується цілим числом, але число для даного кольору може змінюватися від машини до машини і навіть на тій самій машині, від одного запуску програми до іншого. Єдині “кольори”, існування яких гарантує X, це чорний і білий. Ми можемо отримати їх використавши макроси BlackPixel() і WhitePixel().

      int blackColor = BlackPixel(dpy, DefaultScreen(dpy));
      int whiteColor = WhitePixel(dpy, DefaultScreen(dpy));

Як ви вже могли побачити, більшість класів Xlib отримують “дисплей” (значення повернене XOpenDisplay()) у ролі першого аргументу. Хочете знати чому?

Тут все ще залишаються певні фокуси (типу DefaultScreen()), але ми відкладемо їх пояснення на майбутнє4. Зараз ми можемо створити наше вікно.

       // Створюємо вікно

      Window w = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 
				     200, 100, 0, blackColor, blackColor);

На відміну від діалогу, ми використовуємо функцію XCreateSimpleWindow() замість XCreateWindow(). XCreateSimpleWindow() насправді не простіша за XCreateWindow(), (вона хоче лише трохи менше параметрів), але вона використовує менше концепцій, тому ми зараз просто використаємо її. Тут є цілий ряд параметрів, які ще потребують пояснення:

  • dpy є звичайним приєднанням до дисплея (пам’ятайте).
  • DefaultRootWindow(dpy): просто ще один параметр, який досі міг здаватися магічним. Це “батьківське вікно” того вікна, яке ми створюємо. Вікно, створене нами, з’являється всередині свого батька, і обмежується ним (вікно “обрізається” своїм батьком). Ті, хто припустив, виходячи з імені “Default”, що можуть бути інші кореневі (базові) вікна, подумали правильно. Більше про це пізніше. Зараз взяте за замовчуванням базове вікно показує наше вікно на екрані, і дає віконному менеджеру4 шанс оформити вікно.
  • 0, 0 Це координати верхнього лівого кута вікна (початок відліку в X знаходиться у лівому верхньому куті, на відміну від більшості книжок по математиці). Одиницями виміру, як і будь-які одиниці в X, є пікселями (X не підтримує визначені користувачем шкали, на відміну від інших графічних систем типу OpenGL).
    • Не зважаючи на те, що може здатися, вікно має дуже малий шанс з’явитися в 0,0. Причина в тому, що віконний менеджер розташує вікно у визначеній його правилами позиції.
  • 200, 100: це ширина і висота вікна в пікселях.
  • 0: це ширина границі вікна. Нема потреби робити щось з границею, приєднаною віконним менеджером, тому краще поставити ширину рівною нулю.
  • blackColor, blackColor: кольори границі вікна (НЕ тої границі що встановлюється віконним менеджером), і фону вікна відповідно. XCreateSimpleWindow() очищає вікно після створення, XCreateWindow() не робить цього.
      // Ми хочемо отримувати події MapNotify

      XSelectInput(dpy, w, StructureNotifyMask);

Як ми починаємо усвідомлювати, X базується на клієнт-серверній архітектурі. X сервер посилає події клієнту (програмі, яку ми пишемо), щоб інформувати її про модифікації сервера. Їх є багато (вони виникають кожен раз коли вікно створюється, переміщується, маскується, демаскується та в багатьох інших випадках), тому клієнт має повідомити сервер, про ті події, які його цікавлять. Таким засобом, як XSelectInput(), ми говоримо серверу, що ми хочемо отримувати повідомлення про “структурні” зміни вікна. Створення і відображення є, власне,такими змінами. Немає можливості отримувати лише інформацію про відображення, але не про створення, тому ми беремо все. Конкретно в цій програмі ми цікавимося лише подіями “відображення” (grosso modo5, вікно з’являється на екрані).

      // "Відобразити"("map") вікно(Тобто змусити його з'явитися на екрані)

      XMapWindow(dpy, w);

І (знову), це – клієнт-серверна система. Запит на відображення асинхронний, тобто те, що ця інструкція виконалася, ще не означає що вікно насправді відобразилося. Що бути впевненими, ми чекаємо, поки сервер пошле нам подію MapNotify (власне тому ми хочемо бути уважними до таких подій).

      // Створити "Graphics Context" ("Графічний контекст")

      GC gc = XCreateGC(dpy, w, 0, NIL);

Просто ще один фокус. Але необхідність справлятися з ними – причина існування цього курсу…

З певних причин, графічна модель X безстанова, тобто сервер не пам’ятає (поміж іншим) атрибути, такі, як колір малювання, товщина ліній і т.д. Таким чином, ми передаємо серверу всі ці параметри при кожному запиті на малювання. Щоб уникнути передачі двох дюжин параметрів, багато з яких не змінюється від запиту до запиту, X використовує об’єкт з іменем Graphics Context (графічний контекст) чи коротко GC. Ми зберігаємо в графічному контексті всі потрібні параметри. Тут ми хочемо колір для малювання ліній, так званий колір переднього плану:

      // Повідомляємо GC що ми хочемо малювати білим кольором

      XSetForeground(dpy, gc, whiteColor);

Є багато інших параметрів, які використовуються при малюванні ліній, але вони мають розумні значення по замовчуванню.

Все нормально, як на цей момент. Все налаштовано для відображення вікна.

      // Чекаємо на подію MapNotify

      for(;;) {
	    XEvent e;
	    XNextEvent(dpy, &e);
	    if (e.type == MapNotify)
		  break;
      }

Ми ходимо в циклі, отримуючи повідомлення і відкидаючи їх. Коли ми отримуємо MapNotify, виходимо з циклу. Ми можемо отримувати відмінні від MapNotify події з двох причин:

  • Ми встановили маску StructureNotifyMask на отримання подій MapNotify, але ми також можемо отримувати інші події (такі як ConfigureNotify, яка повідомляє, що вікно змінило положення і/або розмір).
  • Деякі події можуть отримуватися, навіть якщо ми не замовляли їх, це так-звані “non-maskable” (такі що не маскуються). GraphicsExpose є однією з них.

Події, що не маскуються, посилаються сервером тільки у відповідь на деякі програмні запити (такі як копіювання області), тому навряд чи вони відбудуться в нашому випадку.

Процедура XNextEvent() є такою, що блокує, тобто, якщо немає повідомлень (подій), програма просто чекає всередині XNextEvent().

Коли цикл закінчується, ми можемо бути майже впевнені в тому, що вікно з’явилося на екрані. Фактично, це може бути не правильним з того моменту, наприклад, коли користувач, за допомогою віконного менеджера, згорнув програму в іконку, але зараз ми припускаємо, що вікно справді з’явилося. Ми можемо малювати нашу лінію:

       // Малюємо лінію
      
      XDrawLine(dpy, w, gc, 10, 60, 180, 20);

Лінія проводиться між точками (10, 60) і (180, 20). (0,0), як завжди, у лівому верхньому куті вікна. Якщо просто “засне” тут, нічого не станеться, бо, на випадок, якщо ви не знаєте, X має клієнт-серверну архітектуру. Таким чином, запит залишиться в клієнта, поки ми не пошлемо його серверу. Це робиться командою XFlush():

         // Послати запит "DrawLine" серверу

      XFlush(dpy);

Уважні читачі могли помітити, що ми не використовували XFlush() раніше, і це не заважало запитам, таким як XMapWindow() потрапляти до сервера. Відповідь полягає в тому, що XNextEvent() виконує неявний XFlush() перед спробою прочитати які-небудь події. Тепер ми отримали нашу лінію, отже ми можемо просто почекати 10 секунд, щоб люди могли оцінити нашу роботу:

      // Чекаємо 10 секунд
      
      sleep(10);

На цей момент все. В наступному уроці ми отримаємо трішки більше взаємодії. Чекайте на продовження6.

prog-2.cc

// Written by Ch. Tronche (http://tronche.lri.fr:8000/)
// Copyright by the author. This is unmaintained, no-warranty free software. 
// Please use freely. It is appreciated (but by no means mandatory) to
// acknowledge the author's contribution. Thank you.
// Started on Thu Jun 26 23:29:03 1997

//
// Xlib tutorial: друга програма
// Показує вікно на екрані і малює в ньому лінію
// якщо ви не розумієте цієї програми, дивіться сюди:
// https://tronche.com/gui/x/xlib-tutorial/2nd-program-anatomy.html
// Від перекладача -- в оригіналі було: 
//    http://tronche.lri.fr:8000/gui/x/xlib-tutorial/2nd-program-anatomy.html
//

#include <X11/Xlib.h> // Кожна Xlib програма мусить включати це
#include <assert.h>   // Я включив це, щоб тестувати повернені  
                      // значення ліниво
#include <unistd.h>   // Отже ми отримаємо профіль на 10 секунд

#define NIL (0)       //  Назва вказівника "ні на що"

main()
{
      // Відкриваємо дисплей

      Display *dpy = XOpenDisplay(NIL);
      assert(dpy);

      // Отримуємо деякі кольори

      int blackColor = BlackPixel(dpy, DefaultScreen(dpy));
      int whiteColor = WhitePixel(dpy, DefaultScreen(dpy));

      // Створюємо нове вікно

      Window w = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 
				     200, 100, 0, blackColor, blackColor);

      // Хочемо отримувати повідомлення (події) MapNotify 

      XSelectInput(dpy, w, StructureNotifyMask);

      // Відобразити вікно (тобто, змусити його з'явитися на екрані)

      XMapWindow(dpy, w);

      // Створити "Графічний контекст" (GC)

      GC gc = XCreateGC(dpy, w, 0, NIL);

      // Повідомляємо GC що ми хочемо малювати білим кольором

      XSetForeground(dpy, gc, whiteColor);

      // Чекаємо на подію MapNotify 

      for(;;) {
	    XEvent e;
	    XNextEvent(dpy, &e);
	    if (e.type == MapNotify)
		  break;
      }

      // Малюємо лінію
      
      XDrawLine(dpy, w, gc, 10, 60, 180, 20);

      // Посилаємо запит "DrawLine" на сервер

      XFlush(dpy);

      // Чекаємо 10 секунд

      sleep(10);
}
Друга програма, під Ubuntu LTS 24.04. Ремарка: оригінальний текст не містить ілюстрацій.
І вона ж, під Mandrake7.

Що означають всі ці розмови про ‘клієнт-сервер’?

Всі кажуть що X має “клієнт-серверну” архітектуру. Напевне, так і є, але що це означає ?

Взагалі, концептуально клієнт-серверна архітектура досить проста, але наслідки можуть бути досить тонкими, особливо для її варіанту, реалізованого в Xlib.

Що таке клієнт-серверна архітектура?

Клієнт-серверна7 архітектура, це загальний механізм керування розподіленими ресурсами, до яких одночасно можуть звертатися декілька програм. У випадку X, розподіленими ресурсами є область малювання і канал вводу. Якщо кожному процесу дозволити записувати в них за його бажанням, може статися випадок, коли декілька процесів намагаються малювати на тому ж самому місці, приводячи до не передбачуваного хаосу. Таким чином, лише одному процесу надається доступ до області малювання: X-серверу. Процеси, які хочуть щось малювати або отримувати ввід, посилають запити X-серверу (вони є “клієнтами”). Вони роблять це комунікаційними каналами. X-сервер виконує запити для клієнтів і пересилає їм результати. Він також може посилати повідомлення без явних запитів клієнтів щоб тримати їх в курсі подій. Ці, послані від свого імені сервером, повідомлення, називаються “подіями” (“events”).

Переваги клієнт-серверної архітектури

Клієнт-серверна архітектура має декілька переваг, значна їх частина базується на можливості запуску сервера і клієнта на різних машинах. Тут є такі переваги:

  • Система на базі клієнт-серверної архітектури може бути дуже живучою: оскільки сервер виконується у власному адресному просторі, він може захистити себе від погано написаних клієнтів. Так, якщо клієнт містить помилку, він зруйнується сам, а сервер і інші клієнти будуть продовжувати працювати, ніби нічого не сталося.
  • Клієнт і сервер не зобов’язані знаходитися на тій же самій машині, тому що ми маємо тут механізм комунікацій.
  • Клієнт і сервер можуть виконуватися на різних машинах, проводячи до кращого (можливо) розподілу навантаження.
  • Клієнт і сервер не мусять виконуватися на однаковій апаратурі, операційній системі, і т.д., дозволяючи їм краще взаємодіяти.

Структура клієнт-серверної архітектури X

Як ми вже згадували, сервер і клієнт спілкуються через комунікаційний канал. Цей канал складається з двох рівнів: низькорівневого, який відповідає за надійну передачу байтів (тобто без втрат і подвоєнь). Цей зв’язок може здійснювати, зокрема іменованими каналами (named pipe) в середовищі UNIX, DECNet зв’язком і, звичайно, TCP/IP з’єднанням.

Вищий шар використовує канал побайтової передачі для реалізації високорівневого протоколу – X протоколу. Цей протокол вказує, як передати серверу запит на створення вікна, малювання графіки, і так далі, і як сервер має відповідати та посилати повідомлення. Сам по собі протокол розділено на різні частини:

  • Як приєднатися і як розірвати зв’язок,
  • як представляти різні типи даних,
  • що таке запити, що вони означають і
  • що таке відповіді і що вони означають.

Чекайте на продовження8.

Виноски

  1. Цей С з 90-х… – коментар перекладача. 

  2. В оригіналі було doubletalk, ChatGPT каже, що це тут можна також перекласти як “тарабарщина”. 

  3. Зауваження перекладача: в оригіналі гарно сказано: “The program starts with the legal stuff”, але тоді я не придумав, як це перекласти. 

  4. В оригіналі є лінк, але він веде в порожнечу – ймовірно, ніколи не матеріалізувався – зауваження перекладача.  2

  5. Цю ‘‘крилату’’ італійсько-латинську фразу грубо можна перекласти як ‘‘грубо кажучи’’ – коментар перекладача. 

  6. Зауваження перекладача – видається, воно так і не з’явилося. 

  7. Зауваження перекладача: зразу згадався популярний тоді анекдот. “Клієнт-серверна архітектура – це як підлітковий секс. Всі про нього говорять, ніхто ним не займається, а хто займається – результатом не задоволені, максимум, надіються, що наступного разу буде краще”. 

  8. Зауваження перекладача – і його ніколи не було.