Практические рецепты плагинов: счётчик и действие чата
Полный C++ плагин без хранения сообщений
Рецепт объединяет receive hook, действие в чате и конфигурацию, прочитанную из storage до любых регистраций. Скачать полный chat_counter.cpp и config.txt. Это самостоятельный исходник с metadata/init exports, а не фрагмент, mock или заглушка. Он использует C-заголовок SDK напрямую, без дополнительных библиотек и C++-обёртки с фиксированными буферами.
При включении плагин считает вызовы receive hook с момента init. Действие Show receive count выводит только суммарное число в журнал с тегом ChatCounter. Число общее для всех чатов, не отдельная статистика выбранного чата. Receive callback не читает text/chat_id/message_id, не сохраняет их, ничего не пишет в журнал и не делает файловых/сетевых вызовов. Это не количество уникальных входящих сообщений, не история аккаунта и не гарантия exactly-once: считается ровно число уведомлений, переданных клиентом.
Счётчик — lock-free std::atomic<unsigned long long> с relaxed-операциями; static_assert ограничивает сборку платформой с такой поддержкой (целевая — x64). Relaxed достаточен для независимого числового счётчика, но не является общей синхронизацией других данных. Переполнение unsigned-счётчика происходит по модулю 2⁶⁴. UI callback только читает число, форматирует короткую строку в стековый буфер и вызывает log_info. Ни worker, ни таймер, ни synchronous TDLib в нём нет.
Конфигурация до регистрации callbacks
Namespace storage — постоянная строка chat_counter, независимо от отображаемого metadata.name. Init вызывает get_storage_dir("chat_counter", ...) и читает config.txt внутри возвращённого каталога, то есть plugins/data/chat_counter/config.txt относительно фактического pluginsDir клиента. Не путайте это место с каталогом DLL/SO или рабочим каталогом компилятора. Для переносов UTF-8 пути на Windows используется std::filesystem::u8path.
enabled=1
- Если файла нет, счётчик включён по умолчанию. Первый init получает/создаёт storage-каталог, но не создаёт config.txt. Скопируйте скачанный образец в этот каталог при выключенном плагине.
enabled=1включает оба callbacks;enabled=0оставляет плагин успешно инициализированным, но без receive hook и действия в чате.- Допускаются ровно одна строка ASCII/UTF-8 без BOM, необязательный LF или CRLF в конце. Пробелы, комментарии и дополнительные строки не поддерживаются. Файл читается в буфер 32 байта: слишком большой/нечитаемый файл и неизвестное значение считаются ошибкой.
- При недоступном/неоднозначно усечённом пути или ошибке конфигурации init возвращает false до регистрации callbacks; журнал содержит только причину, без полного пути или содержимого файла. Полный буфер пути отклоняется, а не используется как потенциально другой файл.
- Изменения конфигурации читаются при следующем init. В рецепте нет live-reload watcher; для предсказуемой проверки выключите плагин и перезапустите клиент. Не редактируйте файл одновременно с init. Счётчик не сохраняется на диск и сбрасывается при новом успешном включённом init.
Важен порядок: сначала проверки API и вся конфигурация, затем публикация callbacks. У ABI нет rollback/unregister. Текущий клиент при init=false закрывает arena, но путь ошибки не является транзакционным откатом всех уже добавленных hooks. Поэтому после попыток регистраций рецепт не возвращает false: при ненулевом статусе пишет Registration failed; disable plugin and restart client и возвращает true, сохраняя библиотеку загруженной для уже опубликованных указателей. Это явная диагностика потенциально частичной регистрации, не обещание, что ошибка «исправлена» или оба hooks работают. Отключите плагин и перезапустите клиент; не повторяйте регистрацию в цикле.
Сборка Linux x86-64
Нужны g++ с полноценным C++17/std::filesystem (например GCC 9+) и Linux x86-64. Рабочий каталог — public/examples репозитория сайта либо каталог распакованного набора с соседними include/ и recipes/. Если скачиваете файлы отдельно, сохраните C-заголовок в include/mandagram_plugin.h, исходник — в recipes/chat_counter.cpp. Эти команды не требуют JVM, TDLib, подключения к аккаунту или линковки с клиентом.
g++ -std=c++17 -O2 -fPIC -shared -Iinclude recipes/chat_counter.cpp -o chat_counter.so
Результат — chat_counter.so в текущем каталоге. Windows DLL этой командой не создаётся. Сборка в WSL даёт Linux SO, не библиотеку для Windows-клиента. Перед распространением учитывайте совместимость glibc/libstdc++ целевой системы.
Сборка Windows x64
Установите Visual Studio Build Tools с workload Desktop development with C++, откройте x64 Native Tools Command Prompt и перейдите в тот же каталог с include/ и recipes/. Команда использует C++17 и exports из PLUGIN_EXPORT; отдельный .def не нужен.
cl /nologo /std:c++17 /O2 /EHsc /LD /Iinclude recipes\chat_counter.cpp /link /OUT:chat_counter.dll
Результат — chat_counter.dll (также возможны .obj/.lib/.exp). Требуется x64 toolchain и совместимая Visual C++ runtime целевого компьютера. Для MinGW-w64 x86-64 с C++17 доступен альтернативный вариант, не смешивайте его артефакты с MSVC:
g++ -std=c++17 -O2 -shared -Iinclude recipes/chat_counter.cpp -o chat_counter.dll
Native-библиотека имеет права процесса клиента. Просмотр metadata — не безопасная статическая проверка: preview загружает библиотеку, запускает её загрузочную инициализацию и вызывает metadata до подтверждения. Собирайте свой исходник или проверяйте происхождение бинарника до открытия preview. Сборка и проверка exports не подтверждают интеграцию с реальным клиентом.
Ожидаемое поведение и граница проверки
Ниже — ожидаемый контракт исходника, а не отчёт о запуске плагина в клиенте. Для установки своего артефакта используйте инструкцию установки; выбирайте DLL/SO своей ОС и архитектуры. Не используйте реальные сессии/личные сообщения в автоматических проверках примера.
- Metadata возвращает статическую структуру
Chat Counter, версия1.0.0; файловая конфигурация и регистрация не запускаются внутри metadata. - При отсутствии config.txt или enabled=1 успешный init пишет
Counter and chat action registered. Регистрируются один receive hook и одно действие с idchat_counter.show_count, labelShow receive count, user_data=0. - До receive-уведомлений действие пишет
Receive callbacks since init: 0. После трёх доставленных этому hook callback —Receive callbacks since init: 3. Текст/ID сообщения и ID выбранного чата не выводятся. Плагин не отправляет сообщений и не меняет их содержимое. - При enabled=0 init возвращает true, пишет
Disabled by config.txt; no callbacks registered, но не добавляет ни одного callback. Неправильная конфигурация приводит к false без регистраций.
Изолированный native-хост может вызвать exports, предоставить временный storage и собственные функции регистрации, затем вызвать сохранённые callbacks с искусственными данными. Такая проверка доказывает поведение артефакта и native-контракт примера, не работу Panama/JVM/HookRegistry, UI-интеграцию или безопасность hot reload клиента. Нет worker, ожидающего завершения после callback, но общий unload-протокол клиента всё равно не обещает drain всех выполняющихся callbacks; отсутствие worker не превращает concurrent unload в доказанно безопасную операцию.
TDLib: обработка ошибок без опасного async-рецепта
execute_tdlib_query синхронен для вызывающего. Перенос реализации в runBlocking(Network) не освобождает UI-поток. Поэтому клик действия, Compose, receive/update callback и method-hook с неизвестными locks — не место для этого вызова. В опубликованном счётчике TDLib вообще не вызывается.
Не заменяйте блокировку UI на std::thread(...).detach(): нет deinit/join/cancel, удержания API arena или предоставленного клиентом executor. Пользователь может выключить плагин, пока поток ещё выполняет native-код или хранит api. Регистрация из такого потока дополнительно теряет scoped-владельца плагина и попадает под ключ global. Самодельный поток не становится безопасным только из-за atomic-флага.
Синхронный вызов до возврата init избегает собственного фонового потока, но блокирует путь загрузки; нет обещания, что это не UI, что сеть уже авторизована или что watchdog разрешает длительное ожидание. Это не универсальная безопасная замена. Практическое ограничение текущего ABI: если ваша функция требует неблокирующего TDLib-запроса из UI с гарантированным временем жизни и отменой, такой протокол должен предоставлять хост; 53 слота его не предоставляют. Не публикуйте «готовый async-пример» без внешнего доказанного lifecycle-контракта.
Если у интеграции уже есть документированный допустимый не-UI контекст и гарантия жизни библиотеки/API на весь вызов, обработка ответа должна быть последовательной:
- Сформировать корректный JSON с экранированием. ID — целые JSON-числа без потери 64 бит; bytes — числовые массивы. Этот bridge не обещает совместимость с форматом официального tdjson во всех полях. Неверные типы могут молча стать default, поэтому отсутствие error ещё не доказывает корректность параметров.
- Заранее выделить достаточный выходной буфер. При n<0 считать вызов bridge ошибочным. При n==capacity−1 считать полноту ответа неизвестной; не парсить усечённый JSON как успех и не повторять mutation ради большего буфера.
- Разобрать JSON полноценным парсером, проверить тип корня и
@type. Положительная длина строки может содержать error: 400 (ввод/поддержка функции), 503 (клиент неактивен), 408 (ожидание send), 500 (исключение моста) либо код TDLib. - При error 408 считать исход операции неизвестным: запрос уже мог уйти и выполниться позже. Этот timeout не отменяет его; автоматический повтор send/edit/delete небезопасен. Семь секунд относятся к ожиданию future после send, не ко всему вызову.
- Для не-error проверить ожидаемый тип и обязательные поля. Не логировать полный JSON, message content или токены. Текст error.message тоже может содержать пользовательские данные: не считать его автоматически безопасным журналом.
Для изучения формата можно использовать безвредное тело {"@type":"getMe"}, но сам вызов всё равно имеет описанные условия по потоку и lifecycle. Это пример входных данных, не скрытый исполняемый network-рецепт и не обещание успешной авторизации.
Method hooks и Compose: конкретная диагностика вместо непроверенного примера
Здесь намеренно нет «готового» перехвата произвольного метода клиента или native Compose-панели: для указанной ревизии не заявлена их проверка исполнением в клиенте. C++ компиляция не доказывает JVM descriptor, трансформацию байткода, Compose masks и допустимость handles. Используйте high-level chat action, если задача не требует изменения внутренних методов.
- Зафиксируйте версию/ревизию своего клиента, x64 ABI и соответствующий C-заголовок. В собранных JAR/классах найдите реальный owner и имя метода; Kotlin top-level функции оказываются в *Kt, suspend/Compose добавляют параметры, а имена могут быть mangled.
- Получите descriptor из байткода командой JDK
javap -classpath path/to/client.jar -p -s fully.qualified.Owner, подставив реальный JAR и owner своей сборки. Если класс в каталоге classes, используйте этот каталог как classpath. Не вычисляйте descriptor только по Kotlin-исходнику и не копируйте имя метода из другой версии. - Сверьте конкретные аргументы/возврат: this_ptr=null у static, примитивы в args представлены JVM-обёртками, а не native int. Before умеет записывать изменённые handles args обратно; After — нет. Нельзя разыменовывать handle или хранить его между callbacks.
- Смотрите диагностику
PluginBridgeиXposedEngine: регистрация с 0 — только добавление hook, не доказательство вызова/успешной instrumentation. Точный descriptor заменяет выбор name-only списка, а не дополняет его. Для наблюдения используйте только обезличенный факт входа/счётчик, не дамп args или результата. - Не основывайте пример на подмене обычного результата в native After: мост не устанавливает hasResultBeenSet, и движок возвращает оригинал. Также native Before с null result сейчас подставляет Kotlin Unit; нельзя обещать корректное возвращение null для произвольного типа.
- Для Compose проверьте настоящий Composer в аргументах, Unit-compatible return и balanced groups. Сообщение
replace_composable: Composer not found in HookParam arguments!— прямой признак неверной цели/аргументов. Для Row/Column/Box нужен scoped Function3, а не unscoped Function2. Modifier.clickable требует Function0, фабрики которой в C API нет. - Сопоставьте доступные Compose overloads через javap с
ComposeInvoker: поиск по имени/числу параметров не доказывает правильность default masks. ОшибкиError invoking Row/Column/Box/Spacer,material3.Text invocation failed,basicText invocation failed,Image invocation failedдиагностируют reflection-путь. Если метод не найден, некоторые вызовы молча ничего не рисуют. font_size_sp в текущем compose_text не применяется.
Эти шаги выполняйте на своей сборке в отдельной тестовой среде без реальных данных. До подтверждения конкретной цели и отрисовки ограничивайте обещания документацией контрактов, а не выдавайте неподтверждённый native UI за рабочий рецепт. Полные параметры, defaults и ограничения каждого вызова приведены в справочнике 53 функций.