Бэкенд GHC wasm поддерживает функцию JavaScript FFI. Для проектов Haskell, предназначенных для выполнения в средах JavaScript, таких как браузеры или nodejs, JavaScript FFI позволяет:
JavaScript FFI, как реализован в бэкенде GHC wasm, разработан проектом asterius и существенно вдохновлен GHCJS, предшественником бэкенда GHC для JavaScript. Несмотря на некоторые сходства, он всё же существенно отличается от реализации бэкенда GHC JavaScript. Остальная часть данного руководства является канонической ссылкой на JavaScript FFI бэкенда GHC wasm, который мы будем сокращать до JSFFI.
15.5.1. Маршализуемые типы и JSVal
JSFFI поддерживает все маршализуемые внешние типы, которые поддерживает C FFI:
Bool Char -
Int / Word
-
Int8 / Int16 / Int32 / Int64
-
Word8 / Word16 / Word32 / Word64
-
Ptr / FunPtr / StablePtr
-
Float / Double
Указанные выше типы и их newtype могут использоваться в качестве типов аргументов/результатов в JSFFI. Необходимо учитывать некоторые моменты:
-
Bool маршализуется в 0 / 1 вместо false / true в JavaScript. Это связано с особенностями реализации JSFFI, которая основана на C FFI и наследует некоторые его характеристики. В большинстве случаев это не должно вызвать проблем, так как неявное преобразование в boolean происходит при использовании в качестве булева значения. Также допустимо передавать JavaScript boolean в Haskell, поскольку он будет неявно преобразован в число. - Аналогично,
Char маршализуется в 32-битное целое число, которое представляет его код Юникода. Не передавайте одиночный символ JavaScript string в качестве Char, так как неявное преобразование в число приведет к NaN! Если вам абсолютно необходимо использовать Char в качестве типа аргумента/результата JSFFI, вы должны сами обрабатывать Char как коды символов. Вероятно, вам нужно только маршализовать между Haskell String или Text и JavaScript string, для чего уже существуют функции преобразования. - 64-битные целочисленные типы маршализуются в JavaScript
bigint. В JavaScript смешивание bigint и обычных чисел в арифметических операциях приводит к ошибкам типа, поэтому учитывайте это. Что касается Int / Word, они являются 32-битными, так как бэкенд GHC wasm основан на wasm32. - JSFFI не поддерживает немаршализуемые внешние типы, такие как
Int#, ByteArray#, и т. д., даже когда UnliftedFFITypes включен.
В дополнение к указанным типам JSFFI поддерживает тип JSVal и его newtype в качестве типов аргументов/результатов. JSVal определен в GHC.Wasm.Prim в ghc-experimental, что представляет собой неявную ссылку на значение JavaScript.
JSVal — это полноценные значения Haskell в куче Haskell. Их можно получить через внешние импорты или внешние экспорты, сохранять в структурах данных Haskell и передавать между Haskell/JavaScript. Они управляются сборкой мусора GHC RTS:
- Может существовать несколько
JSVal , указывающих на одно и то же значение JavaScript. Пока хотя бы один JSVal активен в куче Haskell, соответствующее значение JavaScript будет активным в куче JavaScript. - Если
JSVal , указывающие на значение JavaScript, больше не активны в куче Haskell, после сбора мусора в Haskell среда выполнения не хранит больше никаких ссылок на него, и среда выполнения JavaScript в конечном итоге соберет его мусор.
Помимо сбора мусора, GHC.Wasm.Prim также экспортирует freeJSVal :: JSVal -> IO (), что позволяет пользователю явно удалить ссылку на JavaScript из среды выполнения. Рекомендуется использовать freeJSVal , когда вы уверены в сроке жизни JSVal , особенно для временных JSVal. Это поможет уменьшить потребление памяти во время выполнения.
Обратите внимание, что freeJSVal не идемпотентен, и его безопасно вызывать только один раз или вообще не вызывать. После вызова любое последующее использование данного JSVal приводит к сбою среды выполнения.
15.5.2. Внешние импорты
Можно встроить фрагмент кода JavaScript в объявление внешнего импорта и вызвать этот фрагмент кода JavaScript, вызвав функцию внешнего импорта:
import GHC.Wasm.Prim
foreign import javascript unsafe "console.log($1)"
js_print :: JSString -> IO ()
foreign import javascript unsafe "typeof $1 === 'object'"
js_is_obj :: JSVal -> Bool
foreign import javascript unsafe "let acc = 1; for (let i = 1; i <= $1; ++i) acc *= i; return acc;"
js_fac :: Word -> Word
Фрагмент кода импорта JSFFI может быть либо единственным JavaScript-выражением, либо набором JavaScript-операторов в качестве тела функции, в этом случае вы можете использовать return для возврата значения результата импорта. Фрагмент кода импорта имеет доступ к:
- Значениям аргументов импорта, привязанным к аргументам
$1, $2, и т. д. - Связыванию
__export, содержащему все экспорты модуля wasm. Например, вы можете использовать __exports.memory для доступа к объекту WebAssembly.Memory и использовать его для копирования блоков между сторонами Haskell/JavaScript. Экспорт memory существует по умолчанию. - Полному API веб-технологий, существующему в глобальной области видимости JavaScript.
Существует два типа импортов JSFFI: синхронные/асинхронные импорты. unsafe обозначает синхронные импорты, которые имеют следующие особенности:
- Вызывающий поток, а также вся среда выполнения блокируются при ожидании результата импорта.
- Если код JavaScript вызывает исключение, среда выполнения терпит крах с той же ошибкой. Исключение JavaScript нельзя обработать как исключение Haskell, поэтому вам нужно явно использовать JavaScript
catch при необходимости. - Как и при импорте C
unsafe, повторное вхождение не поддерживается; импортированный внешний код не должен вызывать Haskell повторно. В противном случае среда выполнения прервётся.
Когда импорт JSFFI помечен как safe / interruptible или не имеет аннотации безопасности, он обрабатывается как асинхронный импорт. Асинхронные импорты JSFFI комбинируют модель конкурентности Haskell и цикл событий JavaScript, позволяя коду Haskell работать с асинхронными вычислениями JavaScript без блокировки всей среды выполнения.
import Control.Exception
foreign import javascript safe "new Promise(res => setTimeout(res, $1))"
js_sleep :: Int -> IO ()
sleep :: Int -> IO ()
sleep t = evaluate =<< js_sleep t
foreign import javascript safe "const r = await fetch($1); return r.text();"
js_fetch :: JSString -> IO JSString
Асинхронный код импорта заключен в асинхронные функции JavaScript, поэтому await также поддерживается. Асинхронные JavaScript-функции всегда возвращают Promise , и вы также можете явно создать и вернуть Promise , который решает конечный результат асинхронных вычислений.
Когда вызывается асинхронный импорт JSFFI, функция Haskell возвращается немедленно после возврата асинхронной JavaScript-функции Promise. Возвращаемое значение функции Haskell — это ленивая функция. Когда ленивая функция вычисляется позже, вычисляющий поток приостанавливается средой выполнения и возобновляется, когда Promise фактически разрешается или отклоняется.
По сравнению с синхронными импортами JSFFI, асинхронные импорты JSFFI имеют следующие преимущества и недостатки:
- Ожидание результата блокирует только один поток Haskell; другие потоки могут продолжать работу, и сборка мусора может выполняться.
- Если
Promise отклоняется, код Haskell может перехватывать JavaScript-ошибки в качестве JSException. - Поддерживается повторное вхождение. Код JavaScript может повторно вызывать Haskell и наоборот.
- Конечно, он имеет более высокую стоимость, чем синхронные импорты JSFFI.
Использование ленивых функций для инкапсуляции результата Promise позволяет получить более эффективную конкурентность, не прибегая к разветвлению потоков Haskell только для ожидания возврата нескольких асинхронных вызовов. Как и в случае с ленивым вводом/выводом, удобство сопряжено с неудобствами; необходимо позаботиться о принудительном получении результата ленивой функции перед закрытием связанного ресурса. И даже если тип результата (), это всё равно ленивая функция, которую нужно явно принудительно получить, чтобы убедиться, что Promise фактически разрешен, поэтому для таких случаев, как sleep, вам, вероятно, нужно написать пару функций-работника/обёртки.
Также существует особый тип импорта JSFFI, который позволяет преобразовывать вызываемую JSVal в функцию Haskell:
type Logger = JSString -> IO ()
type JSFunction = JSVal
foreign import javascript unsafe "s => console.log(s)"
js_logger :: JSFunction
foreign import javascript unsafe "dynamic"
js_logger_to_hs :: JSFunction -> Logger
Подобно foreign import ccall "dynamic", которая оборачивает указатель на C-функцию в функцию Haskell, foreign import javascript "dynamic" оборачивает JSVal, представляющую JavaScript-функцию, в функцию Haskell. Возвращаемая Haskell-функция сохраняет ссылку на эту JSVal, а аннотация unsafe / safe указывает, является ли эта JavaScript-функция синхронной или асинхронной.
Конечно, без foreign import javascript "dynamic", можно было бы легко реализовать аналогичную функциональность:
foreign import javascript unsafe "$1($2)"
js_logger_to_hs :: JSFunction -> JSString -> IO ()
И именно так это реализовано внутри. Это обрабатывается как импорт JSFFI с автоматически сгенерированным фрагментом кода, который вызывает первый аргумент, передавая остальные аргументы.