Содержание блога
Финальный код: finish-hello-wasm-sdl3-c.zip

QR-код для запуска на смартфонах:

Демка в браузере
Если вы откроете примеры для начинающих на официальном репозитории SDL3 в папке: examples (кстати, они все на Си), то вы увидите, что все примеры используют следующие четыре обязательные функции, а привычная функция main() отсутствует:
- SDL_AppInit (Initialization) - эта функция срабатывает один раз самой первой.
- SDL_AppEvent (Event Handler) - эта функция срабатывает каждый раз, когда происходит какое-то событие, например, нажатие клавиши, движение мышью, клик мышью, изменение размеров окна и т.д.
- SDL_AppIterate (Main Loop) - эта функция срабатывает множество раз в секунду. Здесь можно рассчитать dt (delta time) и сделать анимацию, скорость которой не будет зависеть от числа кадров в секунду, который поддерживает монитор.
- SDL_AppQuit (Clean up) - эта функция сработает один раз перед закрытием приложения. Здесь нужно располагать код для освобождения ресурсов.
Эти функции позволяют собирать один и тот же код для Android, Desktop и WebAssembly без лишних #ifdef и дублирования логики - например, без специфичного для браузера цикла emscripten_set_main_loop при сборке через Emscripten. То есть кодовая база на SDL3-callbacks будет одинаковая для сборки в .exe, .apk, .wasm и т.д.
Рассмотрим пример 01-clear в файле clear.c. Я попросил Gemini 3 перевести комментарии на русский, не меняя ничего в коде, даже отступы:
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| /*
* Этот пример кода создает окно и рендерер SDL, а затем очищает
* окно разным цветом в каждом кадре, так что вы фактически получите окно,
* цвет которого плавно меняется.
*
* Этот код является общественным достоянием. Не стесняйтесь использовать его в любых целях!
*/
#define SDL_MAIN_USE_CALLBACKS 1 /* использовать функции обратного вызова вместо main() */
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
/* Мы будем использовать этот рендерер для отрисовки в этом окне в каждом кадре. */
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
/* Эта функция запускается один раз при запуске. */
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
SDL_SetAppMetadata("Example Renderer Clear", "1.0", "com.example.renderer-clear");
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("examples/renderer/clear", 640, 480, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderLogicalPresentation(renderer, 640, 480, SDL_LOGICAL_PRESENTATION_LETTERBOX);
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается, когда происходит новое событие (ввод мыши, нажатия клавиш и т. д.). */
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS; /* завершаем программу, сообщая ОС об успешном завершении. */
}
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается один раз за кадр и является сердцем программы. */
SDL_AppResult SDL_AppIterate(void *appstate)
{
const double now = ((double)SDL_GetTicks()) / 1000.0; /* преобразуем миллисекунды в секунды. */
/* выбираем цвет для кадра, который будем рисовать. Трюк с синусоидой заставляет цвета плавно сменять друг друга. */
const float red = (float) (0.5 + 0.5 * SDL_sin(now));
const float green = (float) (0.5 + 0.5 * SDL_sin(now + SDL_PI_D * 2 / 3));
const float blue = (float) (0.5 + 0.5 * SDL_sin(now + SDL_PI_D * 4 / 3));
SDL_SetRenderDrawColorFloat(renderer, red, green, blue, SDL_ALPHA_OPAQUE_FLOAT); /* новый цвет, полная непрозрачность. */
/* очищаем окно цветом отрисовки. */
SDL_RenderClear(renderer);
/* выводим свежеочищенный рендер на экран. */
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается один раз при завершении работы. */
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
/* SDL сама очистит окно/рендерер за нас. */
} |
|
Далее мы по шагам рассмотрим, как собрать этот пример в WASM, запустить в браузере локально и загрузить на бесплатный хостинг Vercel из командной строки.
Код стартового примера
- Скачайте стартовый пример: start-hello-wasm-sdl3-c.zip
- Код стартового примера:
main.c
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| #include <stdio.h>
#include <emscripten.h>
// Функция, которая будет вызываться браузером каждую итерацию (фрейм)
void loop()
{
// Здесь обычно обрабатывается ввод пользователя или обновление кадра
}
int main()
{
printf("Привет из WebAssembly!\n");
// Устанавливает главный цикл приложения.
// 0 — использовать стандартную частоту кадров браузера.
// 1 — имитировать бесконечный цикл (предотвращает выход из main).
emscripten_set_main_loop(loop, 0, 1);
return 0;
} |
|
CMakeLists.txt
| Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| cmake_minimum_required(VERSION 3.21)
project(start-hello-wasm-sdl3-c)
# Устанавливаем стандарт C (обязательно перед add_executable)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Задаем название будущего приложения (в Windows это был бы app.exe, а
# в вебе будет app.js / app.wasm)
add_executable(app)
# Добавляем исполняемый файл в сборку
target_sources(app
PRIVATE
src/main.c
) |
|
index.html
| PHP/HTML | 1
2
3
4
5
6
7
8
9
10
11
12
13
| <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Wasm</title>
</head>
<body>
<script async src="./js/app.js"></script>
</body> |
|
- Эта программа выводит строку "Привет из WebAssembly!" в консоль браузера. Функция main() не должна завершаться в браузере, поэтому используется бесконечный цикл
emscripten_set_main_loop. Но мы не будет использовать эту функцию дальше. Мы будет использовать SDL3-callbacks, то есть 4 функции вместо main(), которые и на Desktop и в браузере
- Примечание. При сборке в Release стартового примера выше у меня получилось приложение весом всего лишь 15.8 KB:
- index.html - 245 bytes
- app.js - 13.9 KB
- app.wasm - 1.68 KB
- Итог - 15.8 KB

- На SDL3 будет около в районе 1 МБ, что тоже мало по современным меркам
- Сделает сборку стартового примера по пошаговой инструкции ниже
Сборка стартового примера
- Проверим работоспособность стартового примера, который мы скачали выше
- Установите
emsdk 4.0.15 по пошаговой инструкции: Установка Emscripten SDK (emsdk) и CMake для сборки C и C++ приложений в Wasm
- После выполнения инструкции выше у вас будут установлены: CMake, emsdk, Node.js и http-server
- Откройте CMD (консоль) в папке стартового примера
- Введите следующие команды по очереди (это команды: конфигурирования, сборки и запуска локального сервера)
| Bash | 1
2
3
| config-web
build-web
http-server -c-1 |
|
- Перейдите в браузер и введите адрес: localhost:8080 и нажмите Enter
- Вы увидите результат работы программы в консоли браузера, если нажмёте: Ctrl+Shift+J в Chrome и Edge, либо Ctrl+Shift+K в FireFox
- Запустите в корне папки проекта вторую консоль, чтобы дальше можно было переконфигурировать и собирать проект в этой консоли, а в первой консоли пусть запущен локальные сервер
Настройка инструментов
- Скачайте архив SDL3-devel-3.4.2-wasm.zip
- Примечание. Эту библиотеку я собрал из исходников с помощью Emscripten SDK 4.0.15 по инструкции: SDL3 для Web (WebAssembly): Сборка библиотек: SDL3, Box2D, FreeType, SDL3_ttf, SDL3_mixer и SDL3_image из исходников с помощью CMake и Emscripten
- Создайте на каком-нибудь локальном диске папку "libs", например, на диске C и разархивируйте скаченный архив в папку "libs":

- Откройте скаченный пример в каком-нибудь редакторе кода, например, Notepad++, а лучше в Sublime Text 4: https://www.sublimetext.com/download. Если вы добавите путь к Sublime Text 4 (ST4) в PATH, то сможете запускать ST4 из консоли командой
subl . и тогда ST4 откроет текущую папку
- Откройте файл
CMakeLists.txt и скопируйте в него следующие настройки и не забудьте заменить путь к SDL3 на свой, если у вас путь отличается:
CMakeLists.txt
| Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| cmake_minimum_required(VERSION 3.21)
project(start-hello-wasm-sdl3-c)
# Устанавливаем стандарт C (обязательно перед add_executable)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Задаем название будущего приложения (в Windows это был бы app.exe, а
# в вебе будет app.js / app.wasm)
add_executable(app)
# Подсказываем CMake, где искать конфигурационные файлы библиотеки SDL3
set(SDL3_DIR "C:/libs/SDL3-devel-3.4.2-wasm/lib/cmake/SDL3")
# Загружаем настройки пакета SDL3 (параметры компиляции и пути к заголовкам)
find_package(SDL3 REQUIRED)
# Привязываем SDL3 к нашему приложению (настройка линковки и путей include)
target_link_libraries(app PRIVATE SDL3::SDL3)
# Добавляем исполняемый файл в сборку
target_sources(app
PRIVATE
src/main.c
) |
|
- Заменим этот код на код из официального примера, который был показан в начале этого сообщения (пример 01-clear), а в самом начале этого сообщение мы перевели комментарии на русский с помощью Gemini
- Добавьте в код строку
SDL_SetRenderVSync(renderer, 1); после создания окна:
main.c
| C | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| /*
* Этот пример кода создает окно и рендерер SDL, а затем очищает
* окно разным цветом в каждом кадре, так что вы фактически получите окно,
* цвет которого плавно меняется.
*
* Этот код является общественным достоянием. Не стесняйтесь использовать его в любых целях!
*/
#define SDL_MAIN_USE_CALLBACKS 1 /* использовать функции обратного вызова вместо main() */
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
/* Мы будем использовать этот рендерер для отрисовки в этом окне в каждом кадре. */
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
/* Эта функция запускается один раз при запуске. */
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
SDL_SetAppMetadata("Example Renderer Clear", "1.0", "com.example.renderer-clear");
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("examples/renderer/clear", 640, 480, SDL_WINDOW_RESIZABLE, &window, &renderer))
{
SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderLogicalPresentation(renderer, 640, 480, SDL_LOGICAL_PRESENTATION_LETTERBOX);
SDL_SetRenderVSync(renderer, 1);
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается, когда происходит новое событие (ввод мыши, нажатия клавиш и т. д.). */
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS; /* завершаем программу, сообщая ОС об успешном завершении. */
}
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается один раз за кадр и является сердцем программы. */
SDL_AppResult SDL_AppIterate(void *appstate)
{
const double now = ((double)SDL_GetTicks()) / 1000.0; /* преобразуем миллисекунды в секунды. */
/* выбираем цвет для кадра, который будем рисовать. Трюк с синусоидой заставляет цвета плавно сменять друг друга. */
const float red = (float)(0.5 + 0.5 * SDL_sin(now));
const float green = (float)(0.5 + 0.5 * SDL_sin(now + SDL_PI_D * 2 / 3));
const float blue = (float)(0.5 + 0.5 * SDL_sin(now + SDL_PI_D * 4 / 3));
SDL_SetRenderDrawColorFloat(renderer, red, green, blue, SDL_ALPHA_OPAQUE_FLOAT); /* новый цвет, полная непрозрачность. */
/* очищаем окно цветом отрисовки. */
SDL_RenderClear(renderer);
/* выводим свежеочищенный рендер на экран. */
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE; /* продолжаем выполнение программы! */
}
/* Эта функция запускается один раз при завершении работы. */
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
/* SDL сама очистит окно/рендерер за нас. */
} |
|
- Примечание. Вы можете отформатировать одой командой из консоли, то есть автоматически расставить отступы с помощью пошаговой инструкции: Консольные команды для форматирования исходного кода на C, C++, C#, Java, JavaScript, HTML и CSS. Сортировка пакетов на Python
- Откройте в редакторе кода файл "public/index.html" и замените его содержимое на следующее:
index.html
| PHP/HTML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<canvas id="canvas"></canvas>
<script async src="./js/app.js"></script>
</body>
</html> |
|
- Введите команды конфигурирования и сборки (на самом деле можно команду build-web, а конфигурирование произойдёт по автомату, потому что CMake автоматически проверяет не был ли изменён файл CMakeLists.txt)
- Перейдите в браузер и введите адрес: localhost:8080 и нажмите Enter
- Обратите внимание, что локальный сервер http-server выводит адреса, например, у меня:
| Bash | 1
2
3
4
5
| Available on:
http://100.116.245.193:8080
http://192.168.56.1:8080
http://127.0.0.1:8080
Hit CTRL-C to stop the server |
|
- Вы можете взять телефон, который подключён к Wi-Fi и ввести в его браузере адрес 192.168.56.1:8080 и приложение запустится на Android
- Загрузите созданное веб-приложение (папку
public на бесплатный хостинг Vercel по следующей инструкции
Сборка в Release и вес SDL3 веб-приложения
При сборке в релиз нужно в файле config-web.bat заменить Debug на Release]:
config-web.bat
| Bash | 1
| emcmake cmake -S . -B dist -DCMAKE_BUILD_TYPE=Release |
|
После этого нужно ввести команду config-web. Собирается медленнее секунд на 10 секунд, чем в Debug, в зависимости от мощности компьютера. В результате у меня получилось приложение весом 982 KB (папка public). Папку public мы будет отгружать на бесплатный хостинг в следующей инструкции
Загрузка веб-приложения на бесплатный хостинг Vercel
- Зарегистрируйтесь в Vercel: https://vercel.com/
Установите Vercel глобально командой:
- В корне проекта создайте файл с именем "vercel.json" со следующим содержимым:
vercel.json
| JSON | 1
2
3
4
5
6
7
8
9
10
11
| {
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }
]
}
]
} |
|
- Примечание. Этот файл нужен для загрузки файла app.wasm
- В консоли из корня проекта выполните команду для входа в Vercel:
- В консоли вы увидите сообщение: "Press [ENTER] to open the browser" ("Нажмите [ENTER], чтобы перейти в браузер")
- Нажмите Enter и вы будете перенаправлены в браузер, где нужно будет нажать кнопку "Allow" ("Позволяю")
- Наберите команду для первой отгрузки приложения на хостинг:
- В консоли будут задаваться вопросы и на все эти вопросы Enter
- Вы будет выдан адрес вашего приложения:
| Code | 1
| Aliased: https://finish-hello-wasm-sdl3-c.vercel.app |
|
- Примечание. К сожалению, иногда Vercel часто становится недоступным в РФ. Как альтернатива, можете загрузить папку
public на бесплатный хостинг GitHub Pages. Я загрузил финальный результат на GitHub Pages -> демка в браузере
Создание QR-кода для быстрого запуска на смартфонах
Мы собрали приложение и загрузили его на бесплатный хостинг. У нас есть ссылка на приложение. Используем эту ссылку, чтобы создать QR-код
- Наберите в поисковике интернета: qr code generator
- Одна из первых ссылок будет указывать на ресурс, на который нужно перейти: https://www.qr-code-generator.com/
- В поле ввода скопируйте ссылку на приложение. QR-код появится справа
- Внизу выберите какой-нибудь способ отображение QR-кода. Нарпимер:

|