Автогенерация привязок Rust–JS с помощью UniFFI

Архивный материал Mozilla (август 2023 г.). Указанные версии, результаты и планы относятся к публикации источника. Первоисточник.

Я работаю в команде Firefox Sync в Mozilla. Четыре года назад мы опубликовали пост в блоге, где описали нашу стратегию по доставке кроссплатформенных компонентов на Rust для синхронизации и хранения данных на всех наших платформах. Идея заключалась в консолидации раздельных реализаций таких функций, как история, логины и синхронизация, которые существовали в Firefox Desktop, Android и iOS.

Мы планировали заменить эти реализации ядром на Rust и набором вручную написанных обёрток для каждого языка платформы: JavaScript для Desktop, Kotlin для Android и Swift для iOS.

С тех пор мы извлекли ряд уроков и были вынуждены изменить стратегию. Оказалось, что создание вручную написанных обёрток на нескольких языках отнимает огромное количество времени. Эти обёртки требовали значительных трудозатрат на написание и, что ещё важнее, становились источником множества серьёзных ошибок.

Эти ошибки было легко пропустить, их трудно было отлаживать, и они часто приводили к авариям. Одно из главных преимуществ Rust — безопасность памяти, но вручную написанные обёртки сводили на нет большую часть этого преимущества.

Чтобы решить эту проблему, мы разработали UniFFI: библиотеку на Rust для автогенерации привязок на других языках. UniFFI позволил нам быстро и безопасно создавать обёртки, но оставалась одна проблема: UniFFI поддерживал Kotlin и Swift, но не JavaScript, который используется в интерфейсе Firefox Desktop. UniFFI помог доставить общие компоненты для Firefox Android и iOS, но Desktop оставался вне досягаемости.

Это изменилось с выходом Firefox 105, когда мы добавили поддержку генерации привязок JavaScript через UniFFI, что позволило нам продолжать работу над видением единого компонента. Этот проект подтвердил некоторые ключевые концепции, которые изначально заложены в UniFFI, но также потребовал расширения UniFFI в нескольких направлениях. В этой статье я пройду через проблемы, которые возникали по пути, и расскажу, как мы с ними справлялись.

Эта попытка уже предпринималась как минимум однажды ранее в Mozilla. Команде тогда удалось реализовать некоторую функциональность, но некоторые части оставались недоступными. Одно из первых осознаний было в том, что общий подход предыдущих попыток, вероятно, не поддерживал те возможности UniFFI, которые мы использовали в наших компонентах.

Означает ли это, что предыдущая работа была провальной? Совершенно нет. Команда оставила после себя ценный набор проектных документов, обсуждений и кода, которые мы изучили и заимствовали. В частности, был ADR, обсуждающий разные подходы, который мы подробно изучили, а также работавшие C++/WebIDL-решения, которые мы перепрофилировали для нашего проекта.

Привязки UniFFI располагаются поверх слоя FFI, использующего C ABI, который мы называем «лесом» (scaffolding). Затем пользовательский API определяется поверх этого scaffolding-слоя на языке интеграции. Это позволяет пользовательскому API поддерживать возможности, которые не выражаются напрямую в C, и делает сгенерированный API идиоматичным и естественным. Однако JavaScript усложняет картину, потому что он не поддерживает прямые вызовы C-функций. Привилегированный код в Firefox мог использовать библиотеку js-ctypes от Mozilla, но её использование устарело.

Предыдущий проект решил эту проблему с помощью C++, который вызывал функции scaffolding, а затем использовал средства генерации кода WebIDL в Firefox для создания JavaScript-API. Инструмент генерации кода был довольно хорош и позволял нам определять пользовательский API с помощью сочетания WebIDL и C++ glue-кода. Однако он был ограничен и не поддерживал все возможности UniFFI.

Наша команда решила использовать тот же инструмент генерации WebIDL, но генерировать только scaffolding-слой вместо полного пользовательского API. Затем мы использовали JavaScript для определения пользовательского API поверх этого слоя, как это делается для других языков. Мы были относительно уверены, что инструмент генерации кода больше не станет сдерживающим фактором, поскольку scaffolding-слой задуман минималистичным и выражаемым в C.

Модель потоков для интерфейсов UniFFI не очень гибкая: все вызовы функций и методов являются блокирующими. На вызывающей стороне лежит ответственность за то, чтобы вызовы не блокировали «не тот» поток. Обычно это означает выполнение вызовов UniFFI в пуле потоков.

Модель потоков для JavaScript-кода фронтенда Firefox столь же негибкая: нельзя блокировать главный поток. Главный JavaScript-поток отвечает за все обновления интерфейса, и его блокировка ведёт к неотзывчивому браузеру. Более того, единственный способ запустить другой поток в JavaScript — использовать Web Workers, но они в настоящее время не используются фронтендовым кодом.

Чтобы разрешить ситуацию «неудержимая сила против непрокладываемого объекта», мы просто перевернули модель UniFFI и сделали все вызовы асинхронными. Это означает, что все функции возвращают промис вместо непосредственного значения.

Модель «все функции асинхронны» кажется разумной, по крайней мере для первых нескольких проектов, в которых мы планируем использовать UniFFI. Однако не все функции действительно должны быть асинхронными — некоторые выполняются достаточно быстро и не блокируют. В будущем мы планируем добавить способ для пользователей выбирать, какие функции являются блокирующими, а какие — асинхронными. Это, вероятно, произойдёт вместе с более общей работой по поддержке асинхронности в UniFFI, так как мы обнаружили, что асинхронное выполнение является проблемой для многих компонентов, использующих UniFFI.

После интеграции поддержки UniFFI в Firefox 105 мы постепенно начали добавлять некоторые Rust-компоненты сгенерированные через UniFFI в Firefox. В Firefox 108 мы добавили движок синхронизации удалённых вкладок на Rust, сделав его первым компонентом, разделяемым Firefox на всех трёх наших платформах. Новый движок вкладок использует UniFFI для генерации JS-привязок на Desktop, Kotlin-привязок на Android и Swift-привязок на iOS.

Мы также продолжаем продвигать стратегию общих компонентов на мобильных платформах. Firefox для iOS исторически отставал от Android по внедрению общих компонентов, но в выпуске Firefox iOS 116 будет использован наш общий компонент sync manager. Это означает, что оба мобильных браузера будут использовать все общие компоненты, которые мы написали до сих пор.

Мы также используем UniFFI для генерации привязок для Glean, телеметрической библиотеки Mozilla, что было несколько нетипичным случаем. Glean не генерирует JS-привязки; он генерирует только scaffolding-API, который оказывается в библиотеке GeckoView, обеспечивающей работу Firefox Android. Firefox Android затем может использовать Glean через сгенерированные Kotlin-привязки, которые ссылаются на scaffolding в GeckoView.

Если вас интересует этот проект или UniFFI в целом, присоединяйтесь к нам в канале #uniffi в чате Mozilla Matrix.

Еще статьи Бена Дин-Кавамуры…

Спасибо! Пожалуйста, проверьте вашу почту, чтобы подтвердить подписку.

Если вы ранее не подтверждали подписку на рассылку, связанную с Mozilla, возможно, вам потребуется это сделать. Проверьте вашу почту или папку спама на наличие письма от нас.