Design: Ограничения функции запуска с экспортированной памятью

Созданный на 30 нояб. 2017  ·  57Комментарии  ·  Источник: WebAssembly/design

Мы экспериментировали с использованием начального раздела в мире C / C ++ / clang / lld:
https://reviews.llvm.org/D40559

У меня возникла проблема с экспортированной памятью и статической инициализацией (или любым кодом в функции запуска). По сути, никакая функция JS, требующая доступа к памяти, не может быть запущена во время запуска, потому что экспортированная память недоступна до завершения функции запуска.

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

Я что-то упускаю?

Все 57 Комментарий

Читая связанный обзор, мне непонятно, что вы пытаетесь выполнить в start . Все ли это до main , но исключая main , или до main включительно? FWIW Я не думаю, что start должен коллировать main , или в любом случае это была первоначальная идея для start .

Тем не менее, кажется, что сегменты могут нормально выполнять все статические глобальные инициализаторы C, но нетривиальные глобальные инициализаторы C ++ обычно не могут быть выполнены, потому что они могут выполнять произвольный пользовательский код, включая системные вызовы. Это действительно хлопотно!

Считаете ли вы, что проблема заключается только в вызовах встроенных программ, пытающихся использовать экспортированную память, или вы думаете, что эти вызовы могут также попытаться повторно войти в WebAssembly? Я думаю, что это достаточно просто, если исправление состоит в том, чтобы экспортировать память до start , а затем экспортировать все остальное. Если вместо этого мы экспортируем все до start тогда я не уверен ...

В то же время start предназначен для моделирования того, что делает "оценка" модуля ES6. В мире, где WebAssembly может участвовать в модулях ES6, что должно «оценивать» / start экспорте?

Что вы имеете в виду под словом «недоступно» при запуске? Предположительно, память, созданная для импорта, может (в принципе) быть доступна через закрытие во время выполнения start. Или есть какая-то раздражающая проверка, которая означает, что вы не можете читать / записывать в память, пока она не будет подключена к полностью созданному экземпляру?

Я согласен с тем, что это затрудняет реализацию системных вызовов - я тоже думал об этой проблеме, когда пытался реализовать некоторые системные вызовы для восходящего потока Musl. Меня не слишком беспокоила проблема с памятью, но проблема импорта кажется более острой - до тех пор, пока экземпляр не будет создан (в конце запуска), у импортированных функций действительно нет возможности обратного вызова в Wasm. На практике это нормально, но может вызвать проблемы у кого-то.

Идеально было бы, чтобы «this» относилось к экземпляру с частично созданным экземпляром, если импорт вызывается во время запуска, чтобы импортированная функция могла вызывать экспорт во время запуска через this.exports.blah. Но это открывает целый ряд проблем с безопасностью, в то время как текущий подход безопасен, хотя и немного ограничивает.

Для системных вызовов это не имеет большого значения. По определению, они находятся в «режиме ядра», и поэтому им не нужно ничего вызывать в «пользовательском режиме» (Wasm). Большинство системных вызовов должны иметь возможность выполнять свои задачи, не касаясь модуля / экземпляра.

@jfbastien - в обзоре LLVM нет особого мнения о том, что запускается при запуске. В настоящее время точка входа LLD по умолчанию - _start (т.е. также запустить main). Я согласен, что это не идеально, я надеюсь, что мы могли бы изменить значение по умолчанию на что-то другое, например _start_wasm чтобы запускать все до main. Это зависит от того, кто запускает компилятор, какую точку входа вы установили, и что касается проблемы Сэма здесь, я не думаю, что имеет значение, запущен main или нет.

@ sbc100 Да, это действительно кажется проблематичным в ближайшем будущем. Модули ES действительно допускают циклический импорт, поэтому, если бы у нас была интеграция с модулем ES , JS, вызываемый wasm, мог бы быть модулем ES, который импортировал вызывающий модуль wasm для получения Memory .

С моей точки зрения проблема может быть решена путем предоставления встроенному устройству доступа к вновь созданной памяти перед вызовом start. @jfbastien мне не ясно, как вы предложите это сделать. Вы можете объяснить?

Что касается того, для чего мы будем использовать стартовую часть, то это еще не решено. Я смотрел на то, чтобы переместить туда статическую функцию инициализации C / C ++ и оставить main для явного запуска встроенным модулем. Но эта проблема существует независимо от того, что мы решим там запустить.

С моей точки зрения проблема может быть решена путем предоставления встроенному устройству доступа к вновь созданной памяти перед вызовом start. @jfbastien мне не ясно, как вы предложите это сделать. Вы можете объяснить?

Я думаю, что то, что мы делаем, должно соответствовать тому, что делают модули ES6: сначала они link , затем evaluate . Это может быть круглое! В нашей реализации мы используем модуль машину ES6, мы первый link здесь , разрешающий экспорт и установив соответствующую таблицу символов. После того, как все модули ES6 связаны (на данный момент есть только один для WebAssembly), происходит вызов evaluate который устанавливает element и segment , и в самом конце мы вызвать функцию start . Это то же самое, что запустить глобальный код модуля ES6 непосредственно перед возвратом к вызову import (и это выполняется в порядке, определяемом link IIUC).

Учитывая это, я думаю, что необходимо сделать так, чтобы доступность экспорта соответствовала тому, что модули ES6 делают для своего собственного экспорта в течение evaluate . Я бы посоветовался по этому поводу с @Constellation .

Я думаю, это тоже помогает исправить то, что вы пытаетесь сделать.

Но из точки зрения JS API POV, как мы можем позволить разработчику получить дескриптор новой памяти до вызова start? У меня создалось впечатление, что экземпляр необходимо вернуть, чтобы получить дескриптор памяти, но что функция запуска гарантированно будет вызвана до этого.

В качестве обходного пути я мог бы переключить все тестовые примеры для импорта их памяти ... что, вероятно, неплохая идея, но, очевидно, другие могут столкнуться с этой проблемой.

Я немного запутался. Если вам нужна двухфазная инициализация, когда некоторая функция инициализации должна быть вызвана после того, как экземпляр был полностью построен и передан, что мешает вам использовать для этого обычную функцию? Или, наоборот, спросите, как семантика функции запуска, выполнение которой вы должны инициировать явным образом, будет отличаться от простого вызова обычной функции?

@rossberg с использованием обычного вызова функции - это альтернативный вариант, и это то, что мы делаем в настоящее время. Просто это означает, что наши соглашения об инструментах (и код, сгенерированный clang / lld) не смогут использовать преимущества раздела wasm start. Я не очень-то сильно отношусь к использованию начального раздела wasm, но я просто хотел поднять здесь проблему на тот случай, если люди сочтут проблематичным то, что C / C ++ не будет его использовать.

Недавно это снова появилось в системе отслеживания ошибок llmv: https://bugs.llvm.org/show_bug.cgi?id=37198

Это снова происходит с WASI: https://github.com/WebAssembly/WASI/issues/19

это не просто проблема с функцией запуска. если вы объявляете функцию в js или wasm, а wasm импортирует ее (esm), эта функция не имеет возможности получить доступ к памяти этого модуля wasm.

Эта проблема относится к случаю, когда память wasm экспортируется из модуля. В случае, когда память wasm импортируется в модуль, кажется разумным предположить, что устройство для внедрения имеет доступ к памяти, поскольку она могла бы понадобиться для создания экземпляра модуля в первую очередь.

Есть третий случай, когда память не импортируется и не экспортируется, но, конечно, в этом случае нет возможности поделиться памятью.

Однако я согласен, что эту проблему, вероятно, следует переименовать, поскольку она относится не только к памяти, но и ко всему, что модуль wasm может захотеть экспортировать.

Оглядываясь назад, мы должны были назвать функцию запуска «функцией инициализации», чтобы указать, что ее цель - инициализация модуля, т. Е. Что модуль не предназначен для доступа до завершения этой инициализации. Разрешение повторного входа во время инициализации вызовет хорошо известные проблемы с раскрытием неинициализированного состояния и тем самым нарушит гарантии инкапсуляции (что является важной особенностью модульной системы Wasm).

Тем не менее, я считаю, что все обсуждаемые случаи можно обработать, если модуль будет экспортировать явную функцию инициализации. Функция запуска не для этих случаев, но это проблема?

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

Я не сильно к этому отношусь, но, возможно, поскольку имя является частью двоичного формата, мы все равно можем переименовать его во что-то другое, например, «init» в текстовом формате и в тексте спецификации?

Я бы не возражал с переименованием в спец. То же самое и с текстовым форматом, но это может быть немного более спорным. Мы могли бы разрешить оба ключевых слова.

функция запуска указана для запуска в обычное время оценки в предложении ESM. кроме того, экспорт чего-то явного для esm вообще не работает, потому что нет ничего, что можно было бы импортировать и запускать. Я действительно сбит с толку. предназначена ли вообще интеграция с esm?

Эта проблема относится к случаю, когда память wasm экспортируется из модуля.

даже если вы это сделаете, случайные функции, импортируемые модулем wasm, не смогут волшебным образом получить доступ к этому экспорту.

Интеграция ESM создает иллюзию циклического связывания, обертывая JS-заглушки вокруг экспорта Wasm, который еще не существует. Но они бросают, если вызывают слишком рано. Эти заглушки можно использовать только после того, как будет создан экземпляр модуля Wasm. Таким образом, даже с ESM инициализация модуля Wasm не является повторной или циклической. Это особенность. :)

Почему импорт пользовательской функции инициализации и ее явный вызов после связывания не работают?

@rossberg

Почему импорт пользовательской функции инициализации и ее явный вызов после связывания не работают?

вы предлагаете что-то подобное?

let id = 0;
const memories = {};
export function register_memory(mem) {
  const i = id++;
  memories[i] = mem;
  return i;
}

export function fd_write(id, fd, ptr, len) {
  const mem = memories[id];
  // ...
}

вам нужно сделать память первоклассным значением, и в этот момент вы можете просто сделать это:

export function fd_write(mem, fd, ptr, len) {
  // ...
}

Интеграция ESM создает иллюзию циклического связывания, обертывая JS-заглушки вокруг экспорта Wasm, который еще не существует.

Мы думали об этом, но когда дело дошло до деталей, это оказалось сложным и неосуществимым. Так что этих заглушек нет в текущем предложении.

@devsnek В частности, если

  • В текущем нестабильном случае все смещения относятся к 0-й памяти, поэтому вам просто нужен экземпляр для экспорта своей памяти по умолчанию (просто с помощью простого оператора экспорта памяти). IIRC, живые экспорты модуля wasm заполняются перед выполнением функции запуска, поэтому, если у вас есть мини-цикл между импортируемым кодом связующего JS и вызываемым wasm, который (циклически) импортирует эту память wasm, то к моменту запуска wasm функция запускается и вызывает клей JS, живая привязка экспортированной памяти wasm заполнена. Но, возможно, я устарел; @littledan поправьте меня, если я ошибаюсь.
  • В будущем я думаю, что чтение / запись из / в линейную память должно быть выражено в терминах привязок Web IDL (или чего-то аналогичного), где диапазоны байтов будут передаваться через некоторый абстрактный тип данных «буфер» / «срез» ( используются в интерфейсе), которые создаются с помощью операторов привязки view / copy (которые выбирают свою память через индекс памяти непосредственно в выражении привязки). Тогда получателю буфера / среза не нужно беспокоиться о поиске нужной памяти для использования.

@lukewagner невозможно получить доступ к какой-либо памяти с помощью esm, независимо от того, является ли он 0-м. Я использую васи в качестве мотивирующего примера, но проблема простирается гораздо дальше.

@littledan :

это оказалось сложным и неосуществимым. Так что этих заглушек нет в текущем предложении.

А, ладно, тогда это просто неинициализированные "живые" привязки? В любом случае эффект должен быть почти таким же.

@lukewagner :

IIRC, прямые экспортные данные модуля wasm заполняются до выполнения функции запуска.

Экспорт модуля заполняется после завершения функции запуска. Раньше у вас были только привязки, но они еще не определены. Но я считаю, что этого достаточно для таких случаев использования, потому что пока нет необходимости обращаться к памяти.

@devsnek Да , если модуль его экспортирует.

@rossberg

Экспорт модуля заполняется после завершения функции запуска. Раньше у вас были только привязки, но они еще не определены

К моменту выполнения функции start экземпляр должен быть полностью живым / сконструированным, поэтому я не вижу какой-либо технической причины, по которой экспорт не мог быть быстро записан в их живые привязки. На самом деле я смутно помню, как обсуждал это в прошлом, поэтому я подумал, что это уже было указано.

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

Это действительно зависит от того, что люди хотят делать во время инициализации, но нетрудно представить, что нужно вызывать JS для какой-то утилиты для склейки и для того, чтобы этот клей JS нуждался в экспортированной памяти для реализации своей функциональности.

@lukewagner, если я экспортирую функцию, предназначенную для использования wasm (из wasm или js), как эта функция получает доступ к памяти вызывающего wasm?

@devsnek Тот же модуль, экспортирующий функцию, должен также экспортировать свою память, и тот же модуль, импортирующий функцию, также должен импортировать память. Конечно, это не идеально, но, как объяснялось выше, это не состояние цели.

@lukewagner файл с функцией не знает, какой модуль wasm использует его, это не та вещь, которую предоставляет esm. (он также не знает имени экспортируемой памяти, но это менее актуально)

@devsnek Я представлял, как обернуть каждый wasm в 1: 1 клеевой JS-модуль. Этот клеящий JS-модуль можно рассматривать как полифил будущей способности модуля wasm явно передавать представление или копию своей памяти в качестве аргумента вызова (например, полифил Web IDL Bindings).

@lukewagner Я про интеграцию esm. похоже на довольно большую дыру в конструкции, так что я очень хочу это исправить. разрешение некоторой формы ссылки первого класса на память или фрагменты памяти (или и то, и другое) устранит как эту проблему с функцией запуска, так и более крупную проблему esm.

@devsnek Я вижу, как это относится к ссылочным типам (в частности, ref.mem ) или привязкам Web IDL (в частности, к оператору привязки view ) и их полифилам, но не напрямую к интеграции ESM. В частности, я думаю, что было бы скорее нарушением инкапсуляции, если бы вызываемый абонент мог запросить память вызывающего абонента, если он явно не передан вызывающим (с помощью одной из этих опций).

согласовано. Я не говорю, что нам нужно нарушить инкапсуляцию, я просто говорю, что нам что-то нужно.

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

Тем не менее, по-прежнему существует общий вопрос об интеграции wasm / ESM: учитывая, что основной экземпляр wasm полностью инициализирован, а выполнение функции запуска - просто сахар для простого вызова экспорта, почему мы не можем разбить module_instantiate на два шага:

  1. создать экземпляр
  2. вызвать функцию запуска

так что интеграция ESM может выполнить 1, захватить экспорт, инициализировать живые привязки, затем выполнить 2. В отличие от включения общего циклического импорта wasm, это будет относительно незначительным рефакторингом спецификации и реализации и позволит избежать этого причудливого промежуточного состояния выполнения для функции запуска.

В качестве альтернативы, спецификация интеграции ESM может вытащить старую подсказку из спецификации, в которой говорится: «между шагами 14 и 15 создания экземпляра вызовите instance_exports с помощью moduleinst и используйте эти значения для инициализации живых привязок ... но это кажется худший.

@lukewagner , я помню, у нас было такое же обсуждение в прошлом году. ;) Мы не должны этого делать по причинам, которые я упомянул выше: это позволит получить доступ к модулям, пока они все еще находятся в неинициализированном состоянии, тем самым нарушая существующие гарантии инкапсуляции.

Состояние выполнения функции запуска совсем не странное, это очень похоже на выполнение конструктора объекта. Вы не хотите, чтобы другой код мог вызывать методы объекта, пока конструктор все еще работает. (Нам действительно стоит переименовать его в функцию инициализации ...)

Если вам нужна двухфазная инициализация, вы должны использовать функцию явной инициализации.

если вы измените порядок с

  1. в этом
  2. функция запуска
  3. esm экспорт

к

  1. в этом
  2. esm экспорт
  3. функция запуска

не будет ли единственная потеря в инкапсуляции, если вы специально нарушите инкапсуляцию в своей стартовой функции, вызвав в мир? ваш модуль полностью инициализирован в соответствии с определением wasm, единственное, что вас останавливает, - это то, что вы определяете как инициализированный, а затем вы должны иметь возможность вызывать и тому подобное. если вы не решите вызвать функцию запуска, я думаю, что это изменение порядка ненаблюдаемо. (и я думаю, что второй порядок также неявен для нативных реализаций, которые связывают символы вместе)

Но мне может потребоваться вызвать некоторые из моих импортов для выполнения части моей инициализации. В таких случаях я был бы вынужден отказаться от инкапсуляционных инвариантов и прибегнуть к защитным проверкам во всех экспортируемых функциях.

OTOH, просто предоставить явную функцию инициализации для случаев, когда я хочу, чтобы мир мог позвонить мне обратно. Это, по крайней мере, делает протокол и связанный с этим риск явным и возлагает бремя защитного программирования на те модули, которые делают этот выбор.

Есть ли у вас вариант использования, который не работает при замене instantiate(...) подходящим вариантом instantiate(...).then(m => { m.exports.init(); return m }) ?

@rossberg вся интеграция с

@rossberg К моменту start экземпляр полностью и полностью работает (по крайней мере, на хосте JS, который является предметом этого обсуждения): сегменты elem записали функции экземпляров в глобально- видимые таблицы. В будущем: ref.mem может таким же образом раскрыть воспоминания. Так что избегать записи экспорта в живые привязки - всего лишь фиговый листок: экземпляр уже повторно вызывается из импорта, вызываемого функцией start .

Учтите также, что импорт не обязательно является «внешним миром»; импорт также может действовать как «системные вызовы», предоставляя необработанные функциональные возможности, которые представляют собой детали реализации инициализации экземпляра, вызывающего их из своей функции запуска. Таким образом, поскольку функция wasm start может вызывать другие функции wasm в качестве деталей реализации инициализации (несмотря на то, что она находится в состоянии «не полностью инициализировано»), она может вызывать импорт как системные вызовы. Само приложение wasm должно сделать это правильно; роль wasm не в том, чтобы пытаться (частично) добиться этого.

Другой способ уравновесить спецификацию, чтобы сохранить экземпляр как единую функцию спецификации верхнего уровня, - это ввести обратный вызов в хост (аналогично hf когда вызывается hostfunc ) между шагами 14 и 15. создания экземпляра.

@lukewagner :

экземпляр уже может быть повторно вызван из импорта, вызываемого функцией start.

Если я не неправильно истолковываю предложение ESM (и не пропустил какое-то центральное обсуждение раньше), я думаю, что это неверно - к счастью, потому что это было бы критическим изменением!

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

Точно! И имеющаяся у нас семантика позволяет это, не нарушая при этом инкапсуляцию.

в момент запуска кода, определенного пользователем, разве инкапсуляция модуля не должна зависеть от решений кода, определяемого пользователем? сильные гарантии - это круто, но они ограничивают функциональность. как вы сказали выше, если вы абсолютно не можете разрешить вызов экспорта в результате вашего вызова (что кажется вдвойне маловероятным сценарием), вы можете поставить охрану в свой код.

Если я не неправильно истолковываю предложение ESM (и не пропустил какое-то центральное обсуждение раньше), я думаю, что это неверно - к счастью, потому что это было бы критическим изменением!

Это ортогонально предложению ESM и уже имеет место с MVP JS API: к моменту вызова функции запуска элементы модуля были записаны в импортированные таблицы. После MVP ссылки на функции и память могут быть переданы импорту, вызываемому при стартовой функции. К моменту вызова функции запуска экземпляр должен быть полностью работоспособной боевой станцией!

Точно! И имеющаяся у нас семантика позволяет это, не нарушая при этом инкапсуляцию.

Но если код функции wasm start вызывает JS в емкости системного вызова, и этому системному вызову JS требуется (экспортированная) память wasm для выполнения того, что ему нужно, то мы на самом деле не позволяем этого.

к моменту вызова функции запуска элементы модуля были записаны в импортированные таблицы.

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

К моменту вызова функции запуска экземпляр должен быть полностью работоспособной боевой станцией!

Но в целом это невыполнимое требование, поскольку вы не можете выполнить даже самые тривиальные вычисления перед функцией запуска, например, сложение двух чисел.

Но если код функции wasm start вызывает JS в емкости системного вызова и этому системному вызову JS требуется (экспортированная) память wasm для выполнения того, что ему нужно, то мы фактически не позволяем этого.

Я согласен. Но почему в таких случаях необходимо использовать функцию запуска?

Верно, но только если модули решат таким образом инициализировать внешнюю таблицу.

Да, но здесь применяется тот же аргумент: если модуль решает вызвать импорт во время функции запуска, он находится в этом модуле, чтобы убедиться, что это допустимое действие.

Но в целом это невыполнимое требование, поскольку вы не можете выполнить даже самые тривиальные вычисления перед функцией запуска, например, сложение двух чисел.

Я говорю, что экземпляр, созданный спецификацией / движком, должен быть полностью функциональным, а не то, что состояние уровня приложения должно быть полностью инициализировано.

Я согласен. Но почему в таких случаях необходимо использовать функцию запуска?

Как деталь реализации wasm-кода, реализующего функцию запуска. Рассмотрим полифил того, что в противном случае было бы встроенным модулем (реализованным хостом): существует множество причин, по которым функция запуска wasm может нуждаться в использовании встроенных функций, и, следовательно, множество причин для вызова полифила.

Да, но здесь применяется тот же аргумент: если модуль решает вызвать импорт во время функции запуска, он находится в этом модуле, чтобы убедиться, что это допустимое действие.

Я считаю, что это совершенно разные категории. Как правило, звонить нельзя. Использование активного сегмента полностью исключено, по крайней мере, теперь, когда это исправлено в массовом предложении.

Как и в OO: вызов функции из конструктора является нормой, выдача this или подобного обычно не рекомендуется (и даже невозможна в некоторых языках).

Я говорю, что экземпляр, созданный спецификацией / движком, должен быть полностью функциональным, а не то, что состояние уровня приложения должно быть полностью инициализировано.

Теперь я запуталась. До сих пор я думал, что мы разделяем цель, заключающуюся в том, что модули должны обеспечивать правильную инкапсуляцию состояния на уровне приложения. Возможность надежной инициализации этого состояния является важной частью этого. Какую альтернативу вы предлагаете?

Как деталь реализации wasm-кода, реализующего функцию запуска. Рассмотрим полифил, который в противном случае был бы встроенным модулем (реализованным хостом)

Не уверен, что вижу разницу между встроенными и обычными модулями, у них должен быть одинаковый доступ.
Но, тем не менее, вы описываете то, что я называю протоколом двухфазной инициализации. Это больше не деталь реализации, потому что она включает изменения состояния на другой стороне (как минимум, сохранение некоторых ссылок). Я согласен с тем, что одной функции запуска недостаточно, чтобы выразить это, но я тоже не думаю, что это проблема.

Если мы придем к выводу, что это так, то мы должны включить явную передачу ссылок для модулей, которые желают это сделать. Нарушение гарантий инкапсуляции для всех модулей, которым просто нужно вызывать какую-либо функцию, не является хорошей альтернативой. Это тоже будет переломным моментом. (Или мы вводим вторую форму функции запуска, которая выполняется позже. Но это возвращает нас к вопросу, почему экспорта недостаточно.)

Я думаю, разница здесь в том, что мы видим, что функция запуска может делать гораздо больше, чем просто настраивать внутреннее состояние, но, похоже, это просто не намерение?

возможно, мы можем переименовать start в init, а затем добавить новую запись start, которая запускается после экспорта? в идеале мы могли бы просто позволить ему переупорядочить и оставить один ... Я не думаю, что это безумие требовать, чтобы модуль сохранял свою инкапсуляцию после того, как он запускает произвольный код.

@rossberg Хорошо, я думаю, теперь я лучше понимаю вашу точку зрения. Я рассматривал выбор a.wasm для импорта и вызова b.js во время функции запуска a.wasm как явный выбор для обработки b.js как расширения a.wasm реализация. Но на самом деле ответ на приведенный ниже вопрос помог мне понять, что, хотя это иногда и имеет место (а именно, сгенерированный связующий код JS), это не всегда так, и поэтому мы не можем рассматривать это «явным».

Кроме того, я изначально подумал: поскольку общий дизайн ESM очень рано заполняется живыми привязками, прецедент уже создан, и мы должны поступить так же с ESM-интеграцией wasm. Однако теперь я вижу контрапункт: было бы здорово, если бы (с правильными привязками ...) один .wasm мог работать на хосте, загружающем ESM, так же хорошо, как и на любом другом хосте; если мы позволим экспорту запускаться перед запуском в мире ESM, это создаст опасность для совместимых инструментов. Дело в том, что WASI использовал явно экспортированную функцию _start (чтобы обойти все эти проблемы).

(Или мы вводим вторую форму функции запуска, которая выполняется позже. Но это возвращает нас к вопросу, почему экспорта недостаточно.)

В контексте интеграции ESM функция start аналогична сценарию верхнего уровня JS-модуля и, следовательно, если вы хотите написать ESM, скажем, в Rust, и запустить определенную функцию Rust, когда сценарий верхнего уровня JS-модуля будет работать (что является разумным желанием сделать, если вы пытаетесь чисто взаимодействовать с экосистемой ESM), вы должны (с текущим дизайном) запустить эту функцию во время функции запуска; если вы поместите этот код в экспорт, у вас не будет контроля над его запуском.

Для объединения этих двух требований мне нравится эта идея (которая фактически соответствует WASI) обозначения специального экспорта, который автоматически вызывается ESM-интеграцией после завершения создания экземпляра (и, следовательно, функции запуска) и выполнения экспорта.

Итак, вопрос в том, как обозначить эту функцию. Один из вариантов - это настраиваемый раздел (проверенный и семантически значимый на хостах, которые его поддерживают, аналогично тому, что предлагается для привязок ), который обозначает экспорт по имени поля экспорта . (Это упростило бы полифил с точки зрения текущего JS API без какой-либо двоичной перезаписи.)

Другой вариант - выбрать волшебное имя экспорта в подмножестве Unicode, которое недопустимо в качестве идентификатора экспорта ESM (поскольку все поля экспорта действительны в JS API, это также будет полифицируемым), но, возможно, это опасно в будущем, поэтому первое лучше.

В любом случае WASI должен использовать тот же способ обозначения своей функции _start чтобы модули WASI могли выполнять правильные действия (tm) на хосте, загружающем ESM.

Разработаны ли соглашения о том, как WASI будет статически связан? Несомненно, статическое связывание библиотеки WASI как части более крупного приложения не должно запускать функцию запуска этого импортированного файла WASI? Или нет? С точки зрения интеграции JS, статическое включение библиотек WASI кажется действительно важным вариантом использования для работы с этими соображениями, когда вам не обязательно нужна модель процесса «запуск до завершения» при импорте, и вы по-прежнему хотите сохранить преимущества изоляция памяти за счет интеграции модуля ES.

@guybedford Я не совсем понимаю, что вы имеете в виду под «статическим связыванием WASI». WASI - это интерфейс, определяемый как встроенный модуль. Таким образом, во время статической ссылки использование WASI модулем wasm отображается как импорт модуля (встроенного модуля WASI), который, как ожидается, каким-то образом будет разрешен хостом во время создания экземпляра модуля. Теперь хост может выбрать реализацию WASI с каким-либо модулем JS или wasm (например, в Интернете с использованием карты импорта ), но это зависит от хоста и зависит от времени создания экземпляра.

@littledan Что вы думаете о предложенном выше дополнении к ESM-интеграции? Это также может быть хорошей темой для личного обсуждения CG во время слота интеграции ESM.

Приносим извинения за задержку.

Вышеупомянутая идея кажется мне хорошей: у нас есть как текущая функция запуска, вызываемая до того, как экспорт станет видимым, так и другая функция, которая вызывается после того, как экспорт доступен. Похоже, это соответствует всем целям, описанным выше. Мне нравится, что это изменение носит чисто локальный характер и не требует второго обхода графа модуля (помимо того, что не требует двоичной перезаписи).

Велосипедный шеддинг: мы могли бы назвать это *start* , что совершенно недопустимо в качестве идентификатора ES, хотя его легко набирать на многих клавиатурах.

Я включу это в свою презентацию для Wasm F2F и как можно скорее получу официальный PR. Приветствуем текущих разработчиков Rollup, Webpack и Node.js, что это изменение находится на рассмотрении: @sokra @guybedford @mylesborins (я намерен

В случае использования Ethereum ( ewasm ) у нас есть та же проблема, и наш способ ее решения заключается в использовании экспортируемой функции с именем main . Мы были бы за то, чтобы следовать согласованному здесь стандарту, а не изобретать нашу собственную версию.

Можно ли было бы расширить StartSection включив в него две стартовые функции:

  • pre-finalization (он же загружается pre-export) - это текущая функция "start"
  • постфинализация - это будет функция "нового старта"

Изучив эту проблему немного подробнее, я вижу, что функции запуска более сложные, чем предполагалось в https://github.com/WebAssembly/design/issues/1160#issuecomment -500960364, поскольку:

  • Может потребоваться некоторая логика, специфичная для встраивающего устройства, для запуска перед вызовом вторичной функции запуска, например, в этой предлагаемой интеграции Node.js / WASI https://github.com/nodejs/node/pull/27850/files#diff - ab9c666b48467c7030bd93aac6d06eb2R184
  • Любое конкретное имя, которое мы выберем, может перекрываться с чем-то в другой среде, поэтому лучше не кодировать его жестко.

Отсюда, я думаю, нам нужно более тщательно изучить, как различные инструментальные цепочки решают эту проблему, вместо того, чтобы предполагать, что простое решение, как предложено выше, будет работать. Если бы вы могли указать указатели на путь инициализации модуля вашей системы или, в идеале, обобщить более подробную информацию в https://github.com/WebAssembly/esm-integration/issues/32 , это было бы действительно полезно.

Это продолжает сбивать с толку людей: https://bugs.llvm.org/show_bug.cgi?id=42713

(Возвращаясь к этому, потому что я думаю, что было бы весьма полезно стандартизировать wasm, чтобы избежать текущих специальных соглашений о встраивании.)

Один из способов обобщить проблему Node.js, на которую указал start_ (лучше название TBD) должна вызываться сразу после того, как экспорт становится видимым, а WebAssembly.instantiate() или new WebAssembly.Instance() просто вызывает start_ сразу после start и перед возвратом нового экземпляра вызывающей стороне экспорт фактически еще не отображается.

Одна идея состоит в том, что эти функции создания экземпляров могут принимать обратный вызов, который передается только что созданному объекту экспорта и вызывается перед функцией start_ . Итак, глядя на пример Node.js, вы могли бы написать:

new WebAssembly.Instance(compiled, reflect.imports, { expose(exports) {
  for (const expt of Object.keys(exports))
    reflect.exports[expt].set(exports[expt]);

  if (usesWasi && exports.memory)
    wasi.setMemory(exports.memory);
});

Если подумать об этом с точки зрения механизма impl pov, этот обратный вызов expose фактически симметричен тому, как мы реализуем ESM-интеграцию: подпрограмма wasm::Module::instantiate движка примет собственный обратный вызов, который был вызван между start и start_ и загрузчик ESM будут использовать этот обратный вызов, чтобы сделать экспорт живым в своих соответствующих привязках ESM. Итак, на самом деле, мы бы просто открыли этот обратный вызов интеграции ESM для JS, когда использовался JS API нижнего уровня.

@littledan @binji WDYT?

Я где-то предлагал эту идею обратного вызова (честно говоря, понятия не имею, где, это было некоторое время назад), и были представлены причины, по которым она была субоптиомальной.

совсем недавно у меня создалось впечатление, что план по исправлению этого состоял в том, чтобы поддерживать передачу живых фрагментов памяти ( fs_read(fd, buf) вместо fs_read(fd, u32IndexToBuf) ) или любых других вещей, которые можно импортировать / экспортировать из васма

Отличный вопрос: передача живых фрагментов действительно избавит от необходимости в wasi.setMemory() . Я полагаю, что встроенная ESM-интеграция с wasm удалит все остальное. Но я думаю, что общая проблема: «Я использую JS API для создания экземпляра, но мне нужно что- то сделать, чтобы экспорт стал« живым »(независимо от моего определения« живого ») перед функцией _start называется "остается".

Мне нравится это направление расследования, и кажется, что это могло бы решить эту конкретную проблему с помощью WASI, но я не очень уверен, отвечая на него в прямом эфире. Я думаю, чтобы сделать это стандартным, а не специфичным для WASI загрузчиком, было бы лучше, если бы мы могли провести обзор инструментальных цепочек Wasm, чтобы выяснить их шаблоны инициализации и посмотреть, решит ли это их проблемы. У меня лично сейчас нет времени проводить такого рода опрос.

Функция start может принимать аргумент: что должно быть started . Тогда имя функции останется. Аргумент по умолчанию будет таким, как сейчас определено в спецификации: скажем, "START_DEFAULT" = "INIT + COMPLETE". Затем, если вы хотите разбить начало на несколько этапов, отдельно вызывайте "START_INIT", "START_COMPLETE" и т. Д.

Иначе я не вижу никаких ограничений функции start как сейчас, когда ее идея состоит в том, чтобы инициализировать только базовую функциональность модуля для себя. Тогда, однако, практически нет функции, которая бы «запускала» модуль после полной его инициализации стандартным способом. Для облегчения этого можно добавить одну функцию run/main .

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

mfateev picture mfateev  ·  5Комментарии

jfbastien picture jfbastien  ·  6Комментарии

chicoxyzzy picture chicoxyzzy  ·  5Комментарии

Artur-A picture Artur-A  ·  3Комментарии

void4 picture void4  ·  5Комментарии