Почему WebAssembly — будущее Интернета?

внешний интерфейс WebAssembly
Почему WebAssembly — будущее Интернета?

О чем будет эта статья?

Узнайте о прошлом и настоящем WebAssembly и о том, как это замечательное творение, призванное сделать Интернет более доступным, работает на протяжении всего жизненного цикла Web/Node.js.

В этой статье вы узнаете о собственном WebAssembly, AssemblyScript, компиляторе Emscripten и о том, как отлаживать программы WebAssembly в браузере.

Наконец, будущее WebAssembly рассматривается, и перечислены некоторые интересные направления развития технологий.

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

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

  • Как компилировать сложные проекты CMake в WebAssembly?
  • Как изучить общий набор рекомендаций по компиляции сложных проектов CMake в WebAssembly?
  • Как совместить с проектом CMake для отладки?

Зачем вам WebAssembly?

динамический языковой каблук

Во-первых, давайте посмотрим на процесс выполнения кода JS:

Выше показана структура движка ChakraCore до Microsoft Edge.В настоящее время движок JS Microsoft Edge переведен на V8.

Общий процесс таков:

  • Получите исходный код JS, передайте его парсеру и сгенерируйте AST
  • Компилятор ByteCode компилирует AST в байт-код (ByteCode)
  • ByteCode входит в транслятор, транслятор построчно переводит байт-код (Interpreter) в машинный код (Machine Code), а затем выполняет

Но на самом деле код, который мы обычно пишем, имеет множество мест, которые можно оптимизировать, например, если одна и та же функция выполняется несколько раз, сгенерированный этой функцией машинный код можно пометить как оптимизированный, а затем упаковать и отправить в JIT. Компилятор (Just-In-Time), следующий. При повторном выполнении этой функции нет необходимости проходить процесс Parser-Compiler-Interpreter, а подготовленный машинный код может выполняться напрямую, что значительно повышает эффективность выполнения кода.

Однако приведенная выше JIT-оптимизация может использоваться только для статически типизированных переменных, таких как функция, которую мы хотим оптимизировать, она имеет только два параметра, и тип каждого параметра определен, но JavaScript является динамически типизированным языком, что также означает , во время выполнения функции тип может меняться динамически, параметров может стать три, а тип первого параметра может измениться с объекта на массив, что приведет к сбою JIT, а парсер-компилятор- Interpreter-Execution необходимо выполнить снова, а два шага Parser-Compiler являются двумя наиболее трудоемкими шагами во всем процессе выполнения кода, поэтому в контексте языка JavaScript Web не может выполнять некоторые высокопроизводительные операции. высокопроизводительные приложения, такие как крупномасштабные игры, видеоклипы и т. д.

Оптимизация статического языка

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

asm.js предоставляет только два типа данных:

  • 32-битное целое число со знаком
  • 64-битное число с плавающей запятой со знаком

Другие, такие как строки, логические значения или объекты, хранятся в памяти как числа, вызываемые через TypedArray. Целые числа и числа с плавающей запятой представлены следующим образом:

ArrayBufferобъект,TypedArrayвид иDataViewПредставление — это интерфейс для управления двоичными данными в JavaScript, который обрабатывает двоичные данные в синтаксисе массива, который в совокупности называется двоичным массивом. Ссылаться наArrayBuffer.

var a = 1;

var x = a | 0;  // x 是32位整数

var y = +a;  // y 是64位浮点数

Функция записывается следующим образом:

function add(x, y) {

  x = x | 0;

  y = y | 0;

  return (x + y) | 0;

}

Вышеупомянутые параметры функции и возвращаемые значения должны объявлять тип, здесь все 32-битные целые числа.

Более того, в asm.js не предусмотрен механизм сборки мусора, операции с памятью контролируются самими разработчиками, а память напрямую читается и пишется через TypedArray:

var buffer = new ArrayBuffer(32768); // 申请 32 MB 内存

var HEAP8 = new Int8Array(buffer); // 每次读 1 个字节的视图 HEAP8

function compiledCode(ptr) {

  HEAP[ptr] = 12;

  return HEAP[ptr + 4];

}  

Из вышеизложенного видно, что asm.js — это строгое подмножество JavaScript, которое требует, чтобы тип переменных определялся и оставался неизменным во время выполнения, и удаляет механизм сборки мусора, которым обладает JavaScript, требуя от разработчиков вручную управлять памятью. Таким образом, JS-движок может выполнять множество JIT-оптимизаций на основе кода asm.js.По статистике скорость выполнения asm.js в браузере составляет около 50% от нативного кода (машинного кода).

Инновации

Но как бы ни был статичен asm.js, он по-прежнему относится к области JavaScript, чтобы избавиться от некоторых трудоемких абстракций верхнего уровня (сборка мусора и т. д.), а выполнение кода также требует двух процессов Parser-Compiler , которые также являются кодом, требующим больше всего времени для выполнения.

Ради максимальной производительности веб-разработчики внешнего интерфейса отказались от JavaScript, создали язык ассемблера WebAssembly, который может напрямую работать с машинным кодом, и напрямую убили Parser-Compiler.В то же время WebAssembly является строго типизированным статическим языком, может максимизировать JIT-оптимизацию WebAssembly делает скорость WebAssembly бесконечно близкой к собственному коду, такому как C/C++.

Эквивалентно следующему процессу:

Первый взгляд на WebAssembly

Мы можем интуитивно понять расположение WebAssembly в сети по картинке:

WebAssembly (также известный как WASM) — это новый языковой формат, который может работать в Интернете. Он отличается небольшим размером, высокой производительностью и высокой переносимостью. также признан W3C. 4-й язык в Интернете.

Есть несколько причин, по которым он похож на JavaScript на нижнем уровне:

  • Выполняется на том же уровне, что и JavaScript: JS Engine, как Chrome V8.
  • Может работать с различными веб-API, такими как JavaScript.

В то же время WASM также может работать в Node.js или другой среде выполнения WASM.

Текстовый формат WebAssembly

На самом деле WASM представляет собой набор двоичных форматов, которые могут выполняться напрямую, но для удобства отображения в текстовых редакторах или инструментах разработчика WASM также разрабатывает «промежуточный» формат.текстовый формат.watили.wastНазовите расширение, затем передайтеwabtи другие инструменты для преобразования WASM в текстовом формате в исполняемый код в двоичном формате для.wasmв расширенном формате.

Давайте посмотрим на кусок кода модуля в текстовом формате WASM:

(module

  (func $i (import "imports" "imported_func") (param i32))

  (func (export "exported_func")

    i32.const 42

    call $i

  )

)

Логика приведенного выше кода следующая:

  • Сначала определите модуль WASM, затем начните сimportsМодуль JS импортирует функциюimported_func, назови это$i, получить параметрыi32
  • Затем экспортируйте файл с именемexported_funcфункцию, вы можете импортировать эту функцию из веб-приложения, например JS, для использования
  • Тогда параметрыi32Передайте 42, затем вызовите функцию$i

Преобразуем приведенный выше текстовый формат в бинарный код через wabt:

  • Скопируйте приведенный выше код в новый с именемsimple.watсохранено в файле
  • использоватьwabtСкомпилируйте и конвертируйте

После того, как вы установили wabt, выполните следующую команду для компиляции:

wat2wasm simple.wat -o simple.wasm

Хотя он преобразован в двоичный, его содержимое нельзя просмотреть в текстовом редакторе.Чтобы просмотреть двоичное содержимое, мы можем добавить-vвозможность сделать вывод содержимого в командной строке:

wat2wasm simple.wat -v

Результат выглядит следующим образом:

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

Попытка WebAssembly как языка программирования

Поскольку приведенные выше двоичные и текстовые форматы не подходят для кодирования, они не подходят для WASM в качестве обычного языка разработки.

Чтобы обойти это ограничение,AssemblyScriptВыйдя на первый план, AssemblyScript — это вариант TypeScript, который добавляетТип веб-сборки ,можно использоватьBinaryenСкомпилируйте его в WebAssembly.

Типы WebAssembly примерно следующие:

  • i32, u32, i64, v128 и т. д.

  • Небольшие целочисленные типы: i8, u8 и т. д.

  • Целочисленный тип переменной: isize, usize и т. д.

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

Встроенные функции:

  • Проверка статического типа:

    • function isInteger<T>(value?: T): boolЖдать

  • Вспомогательные функции:

    • function sizeof<T>(): usizeЖдать

  • Управление WebAssembly:

    • Математические операции

      • function clz<T>(value: T): TЖдать

    • операция памяти

      • function load<T>(ptr: usize, immOffset?: usize): TЖдать

    • поток управления

      • function select<T>(ifTrue: T, ifFalse: T, condition: bool): TЖдать

    • SIMD

    • Atomics

    • Inline instructions

Затем создайте стандартную библиотеку на основе этого набора встроенных функций.

Стандартная библиотека:

  • Globals

  • Array

  • ArrayBuffer

  • DataView

  • Date

  • Error

  • Map

  • Math

  • Number

  • Set

  • String

  • Symbol

  • TypedArray

Например, типичный массив используется следующим образом:

var arr = new Array<string>(10)



// arr[0]; // 会出错 😢



// 进行初始化

for (let i = 0; i < arr.length; ++i) {

  arr[i] = ""

}

arr[0]; // 可以正确工作 😊

Видно, что AssemblyScript добавляет синтаксис, аналогичный TypeScript, в JavaScript, а затем ему необходимо поддерживать требования статической строгой типизации, такой как используемый C/C++.Если он не инициализирован, при выделении памяти будет сообщено об ошибке выполняется.

Есть также некоторые библиотеки расширений, такие как процесс Node.js, крипто и т. д., консоль JS и некоторые StaticArray, куча и т. д., связанные с памятью.

Можно видеть, что с помощью вышеуказанных базовых типов, встроенных библиотек, стандартных библиотек и библиотек расширений, AssemblyScript в основном создает все функции, которые есть у JavaScript.В то же время, AssemblyScript предоставляет синтаксис, аналогичный TypeScript, и строго следует строгому типизированный статический язык в письменной форме спецификация.

Стоит отметить, что, поскольку текущая спецификация модуля ES для WebAssembly все еще находится в черновике, AssemblyScript реализует модуль сам по себе, например экспорт модуля:

// env.ts

export declare function doSomething(foo: i32): void { /* ... 函数体 */ }

Импортируйте модуль:

import { doSomething } from "./env";

Пример большого фрагмента кода с использованием классов:

class Animal<T> {

  static ONE: i32 = 1;

  static add(a: i32, b: i32): i32 { return a + b + Animal.ONE; }



  two: i16 = 2; // 6

  instanceSub<T>(a: T, b: T): T { return a - b + <T>Animal.ONE; } // tsc does not allow this

}



export function staticOne(): i32 {

  return Animal.ONE;

}



export function staticAdd(a: i32, b: i32): i32 {

  return Animal.add(a, b);

}



export function instanceTwo(): i32 {

  let animal = new Animal<i32>();

  return animal.two;

}



export function instanceSub(a: f32, b: f32): f32 {

  let animal = new Animal<f32>();

  return animal.instanceSub<f32>(a, b);

}

AssemblyScript открыл для нас новую дверь. Он может использовать синтаксис в стиле TS и следовать статическим спецификациям строгого типа для эффективного кодирования, и в то же время он может легко работать с API, связанными с WebAssembly/компилятором. может быть скомпилирован Binaryen.Компилятор компилирует его в двоичный файл WASM, а затем получает производительность выполнения WASM.

Благодаря гибкости и производительности ассемблера, экосистема приложений, построенных с помощью ассемблера, начала процветать.Существует большое количество продуктов, созданных с помощью ассемблера, с точки зрения:Woohoo Сборка script.org/built-with-…

Гениальная философия: запускайте код C/C++ в браузере

Хотя появление AssemblyScript значительно улучшило недостатки WebAssembly в плане эффективного кодирования, в качестве нового языка программирования его самым большим недостатком является экология, разработчики и накопление.

Разработчики WebAssembly, очевидно, учли различные идеальные условия при разработке. Поскольку WebAssembly является двоичным форматом, его можно использовать в качестве цели компиляции для других языков. Если компилятор можно построить, он может преобразовать существующий, Если зрелый язык с большое количество разработчиков и сильная экосистема скомпилированы в WebAssembly для использования, то это эквивалентно прямому повторному использованию накопленного за многие годы этого языка, и использованию их для улучшения экосистемы WebAssembly и запуска их в Web и Node.js middle .

К счастью, для C/C++ уже естьEmscriptenТакие отличные компиляторы существуют.

Положение Emscripten в цепочке разработки можно интуитивно объяснить с помощью следующей картинки:

То есть скомпилировать код C/C++ (или Rust/Go и т. д.) в WASM, а затем запустить WASM в среде выполнения браузера (или Node.js) с помощью связующего кода JS, такого как ffmpeg, который использует C для записи аудио и инструменты транскодирования видео. Его можно скомпилировать в Интернет с помощью компилятора Emscripten, а аудио и видео можно транскодировать непосредственно в интерфейсе браузера.

Приведенный выше код JS "Gule" необходим, потому что, если вам нужно скомпилировать C/C++ в WASM и выполнить его в браузере, вы должны реализовать веб-API, который сопоставляется с операциями, связанными с C/C++, чтобы обеспечить эффективное выполнение, Эти связующие коды в настоящее время включают в себя некоторые из наиболее популярных библиотек C/C++, такие какSDL,OpenGL,OpenAL,так же какPOSIXчасть API.

В настоящее время самый большой сценарий использования WebAssembly — это способ компиляции модулей C/C++ в WASM.Наиболее известные примеры:Unreal Engine 4,Unityтаких как большие библиотеки или приложения.

Заменит ли WebAssembly JavaScript?

Ответ - нет.

Фактически, в соответствии с приведенными выше уровнями объяснения, первоначальный дизайн WASM можно разделить на следующие пункты:

  • В максимально возможной степени повторно используйте существующую базовую языковую экологию, например накопление C/C++ в разработке игр, проектировании компиляторов и т. д.
  • Получите почти нативную производительность в Интернете, Node.js или других средах выполнения WASM, то есть браузеры также могут запускать крупномасштабные игры, редактирование изображений и другие приложения.
  • Существует также наибольшая степень совместимости с Интернетом, обеспечивающая безопасность
  • Также в разработке (если вам нужно разрабатывать) легко читать и писать и отлаживаться, что в AssemblyScript идет дальше

Так что из первоначального замысла на роль WebAssembly больше подходит следующая картинка:

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

Веб-фреймворк Rust:GitHub.com/ также для стека/ также…

Глубокое погружение в Emscripten

адрес:GitHub.com/em скрипт en-…

Все следующие демо доступны в репозитории:code.No special.org/Huang Wei. Счет-фактура...оказаться

Звезда: 21,4K

Техническое обслуживание: активно

Emscripten — это набор инструментов кроссплатформенного компилятора с открытым исходным кодом для компиляции C/C++ в WebAssembly, состоящий из LLVM, Binaryen, Closure Compiler и других инструментов.

Основным инструментом Emscripten является Emscripten Compiler Frontend (emcc), который используется для замены некоторых собственных компиляторов, таких как gcc или clang, для компиляции кода C/C++.

Фактически, для того, чтобы почти все переносимые кодовые базы C/C++ можно было скомпилировать в WebAssembly и выполнить в Интернете или на Node.js, Emscripten Runtime фактически обеспечивает совместимость со стандартными библиотеками C/C++ и связанными API для Web/Node.js. Сопоставление API, это сопоставление существует в скомпилированном коде клея JS.

Посмотрите на рисунок ниже: красная часть — это скомпилированный продукт Emscripten, а зеленая часть — некоторая поддержка во время выполнения, которую Emscripten предоставляет для обеспечения возможности выполнения кода C/C++:

Простой опыт «Hello World»

Стоит отметить, что установка наборов инструментов, связанных с WebAssembly, почти всегда предоставляется в виде исходного кода, что может быть связано с привычками экосистемы C/C++.

Чтобы завершить простую программу C/C++, работающую в Интернете, нам сначала нужно установить Emscripten SDK:

# Clone 代码仓库

git clone https: // github . com / emscripten-core / emsdk . git



# 进入仓库

cd emsdk



# 获取最新代码,如果是新 clone 的这一步可以不需要

git pull



# 安装 SDK 工具,我们安装 1.39.18,方便测试

./emsdk install 1.39.18



# 激活 SDK

./emsdk activate 1.39.18



# 将相应的环境变量加入到系统 PATH

source ./emsdk_env.sh



# 运行命令测试是否安装成功

emcc -v # 

Если установка прошла успешно, приведенная выше команда выдаст следующие результаты после запуска:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.39.18

clang version 11.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 613c4a87ba9bb39d1927402f4dd4c1ef1f9a02f7)

Target: x86_64-apple-darwin21.1.0

Thread model: posix

Подготовим исходный код:

mkdir -r webassembly/hello_world

cd webassembly/hello_world && touch main.c

существуетmain.cДобавьте следующий код в:

 #include <stdio.h>



int main() {

  printf("hello, world!\n");

  return 0;

}

Затем используйте emcc для компиляции этого кода C, переключитесь на командную строкуwebassembly/hello_worldкаталог, запустите:

emcc main.c

Приведенная выше команда выведет два файла:a.out.jsа такжеa.out.wasm, последний представляет собой скомпилированный код wasm, а первый — связующий код JS, который обеспечивает среду выполнения для запуска WASM.

Быстрый тест можно выполнить с помощью Node.js:

node a.out.js

будет выводить"hello, world!", мы успешно запускаем код C/C++ в среде Node.js.

Далее попробуем запустить код в веб-среде и изменить скомпилированный код следующим образом:

emcc main.c -o main.html

Приведенная выше команда создаст три файла:

  • main.jsклей код
  • main.wasmWASM-код
  • main.htmlЗагрузите связующий код и выполните некоторую логику WASM.

Для Emscripten существуют определенные правила генерации кода.Подробнее см.:em script en.org/docs/com bulk…

Если вы хотите открыть этот HTML-код в браузере, вам нужно запустить сервер локально, потому что просто открыть его черезfile://Во время доступа по протоколу основные браузеры не поддерживают запросы XHR, а запросы XHR могут выполняться только под HTTP-сервером, поэтому мы запускаем следующую команду, чтобы открыть веб-сайт:

npx serve .

Откройте веб-страницу и посетите localhost:3000/main.html, вы увидите следующие результаты:

При этом в инструментах разработчика будут соответствующие распечатки:

Попробуйте вызвать функции C/C++ в JS.

В предыдущем разделе мы сначала немного познакомились с тем, как запустить программу на C в Web и Node.js, но на самом деле, если мы хотим делать сложные приложения на C/C++, такие как запуск Web Unity, то нам еще предстоит пройти долгий путь. чтобы пойти в котором, JS способен работать в функции C/C++.

Давайте создадим новый в каталогеfunction.cфайл, добавьте следующий код:

 #include <stdio.h>

 #include <emscripten/emscripten.h>



int main() {

    printf("Hello World\n");

}



EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {

    printf("MyFunction Called\n");

}

Стоит отметить, что код, скомпилированный Emscripten, по умолчанию будет вызывать толькоmainфункции, другой код удаляется во время компиляции как «мертвый код», поэтому для использования кода, который мы определили вышеmyFunction, нам нужно добавить перед его определениемEMSCRIPTEN_KEEPALIVEобъявление, чтобы гарантировать, что оно не будет удалено во время компиляцииmyFunctionСвязанная кодовая функция.

нам нужно импортироватьemscripten/emscripten.hзаголовочный файл для использованияEMSCRIPTEN_KEEPALIVEутверждение.

В то же время нам также необходимо улучшить команду компиляции следующим образом:

emcc function.c -o function.html -s NO_EXIT_RUNTIME=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"

Вышеприведенное добавляет два дополнительных параметра:

  • -s NO_EXIT_RUNTIME=1выраженный вmainПосле выполнения функции программа не завершается, а остается исполняемой, что удобно для последующих вызововmyFunctionфункция
  • -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"Это означает экспорт функции времени выполненияccall, эта функция может вызывать функцию программы C в JS

После компиляции нам также нужно изменить сгенерированныйfunction.htmlдобавим нашу логику вызова функции следующим образом:

<html>

  <body>

    <!-- 其它 HTML 内容 -->

    <button class="mybutton">Run myFunction</button>

  </body>

  <!-- 其它 JS 引入 -->

  <script>

      document

        .querySelector(".mybutton")

        .addEventListener("click", function () {

          alert("check console");

          var result = Module.ccall(

            "myFunction", // 需要调用的 C 函数名

            null, // 函数返回类型

            null, // 函数参数类型,默认是数组

            null // 函数需要传入的参数,默认是数组

          );

        });

    </script>

</html>

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

запустить в командной строкеnpx serve .Открытый доступ в браузереhttp://localhost:3000/function.html, Результаты приведены ниже:

выполнять толькоmainфункция:

Попробуйте нажать кнопку для выполненияmyFunctionфункция:

Вы можете видеть, что сначала отображается всплывающее окно с предупреждением, а затем открывается консоль, вы можете видетьmyFunctionрезультат звонка распечатать"MyFunction Called".

Первое знакомство с файловой системой Emscripten

Мы можем использовать libc stdio API в программе C/C++, напримерfopen,fcloseдля доступа к вашей файловой системе, но JS работает в песочнице, предоставляемой браузером, и не может получить прямой доступ к локальной файловой системе. Таким образом, чтобы быть совместимым с программами C/C++ для доступа к файловой системе и по-прежнему нормально работать после компиляции в WASM, Emscripten будет имитировать файловую систему в своем клеевом коде JS и предоставлять API, совместимый с libc stdio.

Давайте воссоздадим имяfile.cпрограмму, добавьте следующий код:

#include <stdio.h>



int main() {

  FILE *file = fopen("file.txt", "rb");

  if (!file) {

    printf("cannot open file\n");

    return 1;

  }

  while (!feof(file)) {

    char c = fgetc(file);

    if (c != EOF) {

      putchar(c);

    }

  }

  fclose (file);

  return 0;

}

Приведенный выше код мы сначала используемfopenдоступfile.txt, а затем прочитать содержимое файла построчно.Если во время выполнения программы возникнет какая-либо ошибка, будет напечатана ошибка.

Создаем новый каталогfile.txtфайл и добавьте следующее:

==

This data has been read from a file.

The file is readable as if it were at the same location in the filesystem, including directories, as in the local filesystem where you compiled the source.

==

Если мы хотим скомпилировать эту программу и убедиться, что она может правильно работать в JS, нам также нужно добавитьpreloadпараметр, загружать содержимое файла в среду выполнения Emscripten заранее, потому что доступ к файлам в таких программах, как C/C++, является синхронной операцией, а JS — асинхронной операцией, основанной на модели событий, а в Интернете вы можете получить доступ только к файлам в виде XHR (Web Worker, Node.js могут обращаться к файлам синхронно), поэтому вам необходимо загрузить файл заранее, чтобы убедиться, что файл готов до того, как код будет скомпилирован, чтобы код C/C++ мог получить прямой доступ файл.

Запустите следующую команду, чтобы скомпилировать код:

emcc file.c -o file.html -s EXIT_RUNTIME=1 --preload-file file.txt

Добавлено выше-s EXIT_RUNTIME=1, по-прежнему обеспечиваяmainПосле выполнения логики программа не завершается.

Затем запустите наш локальный сервер и получите доступhttp://localhost:3000/file.html, вы можете просмотреть результаты:

Попробуйте скомпилировать существующий модуль WebP и использовать

Из приведенных выше трех примеров мы узнали, как базовые функции C/C++, такие как печать, вызовы функций и содержимое, связанное с файловой системой, компилируются в WASM и запускаются в JS, где JS относится конкретно к средам Web и Node.js, через In приведенном выше примере, большинство программ C/C++, написанных вами, могут быть скомпилированы в WASM для использования.

Как мы упоминали ранее, фактически один из самых больших сценариев применения WebAssembly заключается в максимальном повторном использовании существующей языковой экологии, такой как экологические библиотеки C/C++, Эти библиотеки обычно полагаются на стандартную библиотеку C, операционную систему, файл system или другие зависимости, и самая мощная функция Emscripten заключается в том, что он совместим с большинством этих зависимостей.Хотя все еще есть некоторые ограничения, этого достаточно, чтобы его можно было использовать.

простой тест

Далее давайте посмотрим, как скомпилировать существующий, относительно сложный и широко используемый модуль C: libwebp в WASM и разрешить его использование в Интернете. Исходный код libwebp реализован на языке C, и его можно найти вGithubнайди его и узнай об этомДокументация API.

Сначала подготовьте код, запустите следующую команду в нашем каталоге:

git clone https://github.com/webmproject/libwebp

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

Создаем в каталогеwebp.cфайл, добавьте следующее:

#include "emscripten.h"

#include "src/webp/encode.h"



EMSCRIPTEN_KEEPALIVE int version() {

  return WebPGetEncoderVersion();

}

вышеупомянутыйWebPGetEncoderVersionЭто функция получения текущей версии в LibWebp, и мы импортируемsrc/webp/encode.hЗаголовочный файл для получения этой функции, чтобы компилятор нашел этот заголовочный файл при компиляции, нам нужно сообщить компилятору адрес заголовочного файла библиотеки libwebp при компиляции, и поместить все файлы C под библиотеку libwebp, которая файл, который нужен компилятору, передается компилятору.

Запустим команду компиляции следующим образом:

emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \

 -I libwebp \

 webp.c \

 libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

Приведенные выше команды в основном выполняют следующую работу:

  • -I libwebpСообщите компилятору адрес заголовочных файлов библиотеки libwebp
  • libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.cПередайте файлы C, необходимые компилятору, компилятору, здесь будетdec,dsp,demux,enc,mux,utilsВсе файлы C в каталоге передаются компилятору, что позволяет избежать утомительного перечисления необходимых файлов один за другим, а затем позволить компилятору автоматически идентифицировать эти неиспользуемые файлы и отфильтровать их.
  • webp.cэто функция C, которую мы написали для вызоваWebPGetEncoderVersionПолучить версию библиотеки
  • -O3Представляет собой оптимизацию уровня 3 во время компиляции, включая встраивание функций, удаление бесполезного кода, выполнение различных оптимизаций сжатия кода и т. д.
  • а также-s WASM=1На самом деле это значение по умолчанию, которое выводится во время компиляции.xx.out.wasm, причина, по которой этот параметр установлен здесь, в основном для тех сред выполнения, которые не поддерживают WASM, вы можете установить-s WASM=0, выводить эквивалентный код JS вместо WASM
  • EXTRA_EXPORTED_RUNTIME_METHODS= '["cwrap"]'это функция, которая выводит время выполненияcwrap,аналогичныйccallФункции C можно вызывать из JS

Приведенный выше вывод компиляции толькоa.out.jsа такжеa.out.wasm, нам также нужно создать документ HTML, чтобы использовать код выходного скрипта, создать новыйwebp.html, добавьте следующее:

<html>

  <head></head>

  <body></body>

  <script src="./a.out.js"></script>

    <script>

      Module.onRuntimeInitialized = async _ => {

        const api = {

          version: Module.cwrap('version', 'number', []),

        };

        console.log(api.version());

      };

    </script>

</html>

Стоит отметить, что мы обычноModule.onRuntimeInitializedдля выполнения наших операций, связанных с WASM, в обратном вызове, потому что для загрузки и доступности кода, связанного с WASM, требуется некоторое время, иonRuntimeInitializedОбратный вызов должен убедиться, что код, связанный с WASM, загружен и доступен.

Тогда мы можем запуститьnpx serve ., затем посетитеhttp://localhost:3000/webp.html, просмотрите результат:

66049 Консоль Вы можете увидеть версию для печати.

libwebp через шестнадцатеричный код0xabcabc для указания текущей версииa.b.c, например v0.6.1, он будет закодирован как шестнадцатеричный0x000601, что соответствует 1537 в десятичном виде. А вот 66049 в десятичном виде, а при переводе в шестнадцатеричный получается0x010201, что указывает на то, что текущая версия — v1.2.1.

Получите изображение в JavaScript и поместите его в wasm для запуска

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

API кодирования libwebp должен получать массив байтов RGB, RGBA, BGR или BGRA, к счастью, у Canvas API естьCanvasRenderingContext2D.getImageDataметод, который возвращаетUint8ClampedArray, этот массив содержит данные изображения в формате RGBA.

Сначала нам нужно написать функцию для загрузки изображения на JavaScript и записать его в файл HTML, созданный на предыдущем шаге:

<script src="./a.out.js"></script>

<script>

  Module.onRuntimeInitialized = async _ => {

    const api = {

      version: Module.cwrap('version', 'number', []),

    };

    console.log(api.version());

  };

  

   async function loadImage(src) {

     // 加载图片

      const imgBlob = await fetch(src).then(resp => resp.blob());

      const img = await createImageBitmap(imgBlob);

      

      // 设置 canvas 画布的大小与图片一致

      const canvas = document.createElement('canvas');

      canvas.width = img.width;

      canvas.height = img.height;

      

      // 将图片绘制到 canvas 上

      const ctx = canvas.getContext('2d');

      ctx.drawImage(img, 0, 0);

      return ctx.getImageData(0, 0, img.width, img.height);

    }

</script>

Теперь осталось скопировать данные изображения из JavaScript в wasm.Для этого необходимоwebp.cФункция предоставляет дополнительные методы:

  • Метод выделения памяти для изображений в wasm
  • способ освободить память

Исправлятьwebp.cследующим образом:

#include <stdlib.h> // 此头文件导入用于分配内存的 malloc 方法和释放内存的 free 方法



EMSCRIPTEN_KEEPALIVE

uint8_t* create_buffer(int width, int height) {

  return malloc(width * height * 4 * sizeof(uint8_t));

}



EMSCRIPTEN_KEEPALIVE

void destroy_buffer(uint8_t* p) {

  free(p);

}

create_bufferВыделите память для изображений RGBA, изображения RGBA содержат 4 байта на пиксель, поэтому код необходимо добавить4 * sizeof(uint8_t),mallocУказатель, возвращаемый функцией, указывает на адрес первого блока памяти выделенной памяти.Когда этот указатель возвращается в JavaScript, он будет рассматриваться как простое число. когда прошлоcwrapКогда функция получает соответствующую функцию C, доступную для JavaScript, она может использовать этот номер указателя, чтобы найти начальное местоположение памяти скопированных данных изображения.

Мы добавляем дополнительный код в файл HTML следующим образом:

<script src="./a.out.js"></script>

<script>

  Module.onRuntimeInitialized = async _ => {    

    const api = {

      version: Module.cwrap('version', 'number', []),

      create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),

      destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),

      encode: Module.cwrap("encode", "", ["number","number","number","number",]),

      free_result: Module.cwrap("free_result", "", ["number"]),

      get_result_pointer: Module.cwrap("get_result_pointer", "number", []),

      get_result_size: Module.cwrap("get_result_size", "number", []),

    };

    

    const image = await loadImage('./image.jpg');

    const p = api.create_buffer(image.width, image.height);

    Module.HEAP8.set(image.data, p);

    

    // ... call encoder ...

    

    api.destroy_buffer(p);

  };

  

   async function loadImage(src) {

     // 加载图片

      const imgBlob = await fetch(src).then(resp => resp.blob());

      const img = await createImageBitmap(imgBlob);

      

      // 设置 canvas 画布的大小与图片一致

      const canvas = document.createElement('canvas');

      canvas.width = img.width;

      canvas.height = img.height;

      

      // 将图片绘制到 canvas 上

      const ctx = canvas.getContext('2d');

      ctx.drawImage(img, 0, 0);

      return ctx.getImageData(0, 0, img.width, img.height);

    }

</script>

Вы можете видеть, что приведенный выше код добавляется перед импортомcreate_bufferа такжеdestroy_bufferКроме того, есть много функций для кодирования файлов и т. д., которые мы объясним позже.Кроме того, код сначала загружаетimage.jpgизображение, а затем вызовите функцию C, чтобы выделить память для данных этого изображения, получить возвращенный указатель и передать его в WebAssembly.Module.HEAP8, в начале памяти p записать данные изображения и, наконец, освободить выделенную память.

кодировать картинки

Теперь, когда данные изображения загружены в память wasm, вы можете вызвать метод encoder libwebp для завершения процесса кодирования.Документация для WebP, обнаружил, что вы можете использоватьWebPEncodeRGBAфункция для выполнения работы. Эта функция получает указатель на данные изображения и его размер, и каждый раз, когда вам нужно выполнить span.strideРазмер шага, здесь 4 байта (RGBA), необязательный параметр качества в интервале 0-100. В процессе кодированияWebPEncodeRGBAВыделит блок памяти для выходных данных, нам нужно вызвать после завершения кодированияWebPFreeосвободить эту память.

мы открытыwebp.cфайл, добавьте следующий код для обработки кодировки:

int result[2];



EMSCRIPTEN_KEEPALIVE

void encode(uint8_t* img_in, int width, int height, float quality) {

  uint8_t* img_out;

  size_t size;



  size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);



  result[0] = (int)img_out;

  result[1] = size;

}



EMSCRIPTEN_KEEPALIVE

void free_result(uint8_t* result) {

  WebPFree(result);

}



EMSCRIPTEN_KEEPALIVE

int get_result_pointer() {

  return result[0];

}



EMSCRIPTEN_KEEPALIVE

int get_result_size() {

  return result[1];

}

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

Теперь, когда соответствующая логика стороны C написана, вы можете вызвать функцию кодирования на стороне JavaScript, чтобы получить указатель данных изображения и размер памяти, занимаемый изображением, сохранить данные в буфер WASM, а затем освобождаем wasm при обработке изображения. Имея выделенную память, давайте откроем файл HTML, чтобы завершить логику, описанную выше:

<script src="./a.out.js"></script>

<script>

  Module.onRuntimeInitialized = async _ => {    

    const api = {

      version: Module.cwrap('version', 'number', []),

      create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),

      destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),

      encode: Module.cwrap("encode", "", ["number","number","number","number",]),

      free_result: Module.cwrap("free_result", "", ["number"]),

      get_result_pointer: Module.cwrap("get_result_pointer", "number", []),

      get_result_size: Module.cwrap("get_result_size", "number", []),

    };

    

    const image = await loadImage('./image.jpg');

    const p = api.create_buffer(image.width, image.height);

    Module.HEAP8.set(image.data, p);

    

    api.encode(p, image.width, image.height, 100);

    const resultPointer = api.get_result_pointer();

    const resultSize = api.get_result_size();

    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);

    const result = new Uint8Array(resultView);

    api.free_result(resultPointer);

    

    api.destroy_buffer(p);

  };

  

   async function loadImage(src) {

     // 加载图片

      const imgBlob = await fetch(src).then(resp => resp.blob());

      const img = await createImageBitmap(imgBlob);

      

      // 设置 canvas 画布的大小与图片一致

      const canvas = document.createElement('canvas');

      canvas.width = img.width;

      canvas.height = img.height;

      

      // 将图片绘制到 canvas 上

      const ctx = canvas.getContext('2d');

      ctx.drawImage(img, 0, 0);

      return ctx.getImageData(0, 0, img.width, img.height);

    }

</script>

В приведенном выше коде мы передаемloadImageФункция загружает локальныйimage.jpgкартинку, нужно заранее подготовить картинкуemccОн используется в каталоге, выводимом компилятором, то есть в нашем каталоге файлов HTML.

Уведомление:new Uint8Array(someBuffer)создаст новое представление в том же блоке памяти иnew Uint8Array(someTypedArray)просто скопируйsomeTypedArrayОбязательно используйте скопированные данные для операций без изменения исходных данных памяти.

Когда ваш образ относительно большой, потому что wasm не может автоматически увеличивать память, если выделенная по умолчанию память не может вместитьinputа такжеoutputВ памяти данных изображения могут возникнуть следующие ошибки:

Но изображение, используемое в нашем примере, относительно маленькое, поэтому нам нужно только добавить параметр фильтра во время компиляции.-s ALLOW_MEMORY_GROWTH=1Просто игнорируйте это сообщение об ошибке:

emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \

    -I libwebp \

    webp.c \

    libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \

    -s ALLOW_MEMORY_GROWTH=1

Запустите приведенную выше команду еще раз, чтобы получить код wasm с добавленной функцией кодирования и соответствующий связующий код JavaScript, чтобы при открытии файла HTML он мог кодировать файл JPG в формат WebP. Чтобы еще раз подтвердить этот момент, мы можем Чтобы отобразить картинку в веб-интерфейсе, изменив HTML-файл, добавьте следующий код:

<script>

  // ...

    api.encode(p, image.width, image.height, 100);

    const resultPointer = api.get_result_pointer();

    const resultSize = api.get_result_size();

    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);

    const result = new Uint8Array(resultView);

    

    // 添加到这里

    const blob = new Blob([result], {type: 'image/webp'});

    const blobURL = URL.createObjectURL(blob);

    const img = document.createElement('img');

    img.src = blobURL;

    document.body.appendChild(img)

    

    api.free_result(resultPointer);

    

    api.destroy_buffer(p);

</script>

Затем обновите браузер, вы должны увидеть следующий интерфейс:

Скачав этот файл локально, вы увидите, что его формат преобразован в WebP:

С помощью описанного выше процесса мы успешно скомпилировали существующую библиотеку libwebp C в wasm, преобразовали изображение JPG в формат WebP и отобразили его в веб-интерфейсе Использование wasm для выполнения операций транскодирования, требующих больших вычислительных ресурсов, может значительно повысить производительность веб-страниц. Производительность, которая является одним из основных преимуществ WebAssembly.

Как скомпилировать FFmpeg в WebAssembly?

Хороший парень, только что выучил 1+1 и начал решать квадратные уравнения. 🌚

В предыдущем примере мы успешно скомпилировали существующий модуль C в WebAssembly, но есть много более крупных проектов, которые зависят от стандартной библиотеки C, операционной системы, файловой системы или других зависимостей, и эти проекты зависят от таких библиотек, как autoconfig/automake перед компиляцией. для генерации системного кода.

Итак, вы часто видите, что некоторые библиотеки должны пройти следующие шаги, прежде чем их можно будет использовать:

./configure # 处理前置依赖

make # 使用 gcc 等进行编译构建,生成对象文件

Эмскриптен предлагаетemconfigureа такжеemmakeчтобы инкапсулировать эти команды и ввести соответствующие параметры, чтобы сгладить эти проекты с предзависимостями.Если emcc используется для обработки этих проектов с большим количеством предзависимостей, команды станут следующими:

emmconfigure ./configure # 将配置中的默认编译器,如 gcc 替换成 emcc 编译器

emmake make # emmake make -j4 调起多核编译,生成 wasm 对象文件,而非传统的 C 对象文件

emcc xxx.o # 将 make 生成的对象文件编译成 wasm 文件 + JS 胶水代码

Далее мы объясним, как справиться с этой зависимостью от библиотек, таких как autoconfig/automake, для генерации определенного кода путем фактической компиляции ffmpeg.

Практика показала, что компиляция ffmpeg зависит от конкретной версии ffmpeg, версии Emscripten, среды операционной системы и т. д., поэтому следующая компиляция ffmpeg ограничена конкретными условиями, в основном для обеспечения общей компиляции ffmpeg в будущем. Идеи и методы отладки.

Подготовить каталог

На этот раз мы создаем каталог WebAssembly, а затем помещаем в этот каталог исходный код ffmpeg, а также соответствующий код для декодера x264, который будет использоваться позже:

mkdir WebAssembly



# Clone 代码仓库

git clone https: // github . com / emscripten-core / emsdk . git



# 进入仓库

cd emsdk



# 获取最新代码,如果是新 clone 的这一步可以不需要

git pull

Шаг компиляции

При компиляции наиболее сложных библиотек C/C++ с помощью Emscripten необходимо выполнить три основных шага:

  1. использоватьemconfigureзапуск проектаconfigureфайл для преобразования компилятора кода C/C++ изgcc/g++заменитьemcc/em++
  2. пройти черезemmake makeДля создания проекта C / C ++ генерируют объект WASM.oдокумент
  3. передачаemccПолучение скомпилированных объектных файлов.oфайл, затем выведите окончательный код соединения WASM и JS

Установить определенные зависимости

Примечание: на этом шаге мы уже установили соответствующую версию в начале Emscripten, здесь мы просто выделяем версию.

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

Установить первым1.39.18версии компилятора Emscripten, перед входом мы клонировали локальный проект emsdk и выполнили следующую команду:

./emsdk install 1.39.18

./emsdk activate 1.39.18

source ./emsdk_env.sh

Убедитесь, что переключение прошло успешно, введя следующую команду в командной строке:

emcc -v # 输出 1.39.18

Ветка загрузки на том же уровне emsdk естьn4.3.1Код ffmpeg:

git clone --depth 1 --branch n4.3.1 https://github.com/FFmpeg/FFmpeg

Обработка файлов конфигурации с помощью emconfigure

Обрабатывается следующим скриптомconfigureдокумент:

export CFLAGS="-s USE_PTHREADS -O3"

export LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432"



emconfigure ./configure \

  --target-os=none \ # 设置为 none 来去除特定操作系统的一些依赖

  --arch=x86_32 \ # 选中架构为 x86_32                                                                                                                

  --enable-cross-compile \ # 处理跨平台操作

  --disable-x86asm \  # 关闭 x86asm                                                                                                                

  --disable-inline-asm \  # 关闭内联的 asm                                                        

  --disable-stripping \ # 关闭处理 strip 的功能,避免误删一些内容

  --disable-programs \ # 加速编译

  --disable-doc \  # 添加一些 flag 输出

  --extra-cflags="$CFLAGS" \

  --extra-cxxflags="$CFLAGS" \

  --extra-ldflags="$LDFLAGS" \                  

  --nm="llvm-nm" \  # 使用 llvm 的编译器                                                             

  --ar=emar \                        

  --ranlib=emranlib \

  --cc=emcc \ # 将 gcc 替换为 emcc

  --cxx=em++ \ # 将 g++ 替换为 em++

  --objcc=emcc \

  --dep-cc=emcc 

Приведенный выше скрипт в основном выполняет следующие действия:

  • USE_PTHREADSвключиpthreadsслужба поддержки
  • -O3Указывает, что размер кода оптимизируется во время компиляции, который обычно может быть сжат с 30 МБ до 15 МБ.
  • INITIAL_MEMORYУстановите значение 33554432 (32 МБ), главным образом потому, что Emscripten может занимать 19 МБ, поэтому установите больший объем памяти, чтобы избежать проблемы с нехваткой памяти, которая может быть выделена во время компиляции.
  • фактическое использованиеemconfigureнастроитьconfigureфайл, заменитьgccКомпиляторemcc, и установите некоторые необходимые операции для устранения ошибок компиляции, которые могут возникнуть, и, наконец, сгенерируйте файл конфигурации для компиляции и сборки.

Используйте emmake make для создания зависимостей

После вышеперечисленных действий файл конфигурации обрабатывается, далее необходимо построить собственно зависимости через emmake, выполнив в командной строке следующую команду:

# 构建最终的 ffmpeg.wasm 文件

emmake make -j4

В ходе вышеуказанной компиляции будут сгенерированы следующие четыре файла:

  • ffmpeg
  • ffmpeg_g
  • ffmpeg_g.wasm
  • ffmpeg_g.worker.js

Первые два — это файлы JS, третий — модуль wasm, а четвертый — функция, которая обрабатывает логику в воркере.В идеале сгенерированных выше файлов должно быть три. Необходимо индивидуальное использованиеemccкоманда для обработки.

Скомпилируйте вывод с помощью emcc

существуетFFmpegСоздано в каталогеwasmПапка, используемая для размещения файлов после сборки, а затем вывод настраиваемого скомпилированного файла выглядит следующим образом:

mkdir -p wasm/dist



emcc \                   

 -I. -I./fftools \  

  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \

  -Qunused-arguments \    

  -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c \

  -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm \

  -O3 \                

  -s USE_SDL=2 \    # 使用 SDL2

  -s USE_PTHREADS=1 \

  -s PROXY_TO_PTHREAD=1 \ # 将 main 函数与浏览器/UI主线程分离  

  -s INVOKE_RUN=0 \ # 执行 C 函数时不首先执行 main 函数           

  -s EXPORTED_FUNCTIONS="[_main, _proxy_main]" \

  -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" \

  -s INITIAL_MEMORY=33554432

Приведенный выше скрипт в основном имеет следующие улучшения:

  1. -s PROXY_TO_PTHREAD=1установить во время компиляцииpthread, сделайте так, чтобы программа имела адаптивные спецэффекты
  2. -o wasm/dist/ffmpeg-core.jsоригиналffmpegвывод файла js переименован вffmpeg-core.js, соответствующий выводffmpeg-core.wasmа такжеffmpeg-core.worker.js
  3. -s EXPORTED_FUNCTIONS="[_main, _proxy_main]"Экспортируйте файл C, соответствующий ffmpegmainфункция,proxy_mainустановивPROXY_TO_PTHREAD играет рольmainфункция для внешнего использования
  4. -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"Это экспорт некоторых вспомогательных функций времени выполнения для экспорта функций C, обработки файловых систем и операций с указателями.

Следующие три файла, наконец, выводятся с помощью приведенной выше команды компиляции:

  • ffmpeg-core.js
  • ffmpeg-core.wasm
  • ffmpeg-core.worker.js

Используйте скомпилированный модуль ffmpeg wasm

существуетwasmСоздано в каталогеffmpeg.jsфайл и пишем в нем следующий код:

const Module = require('./dist/ffmpeg-core.js');



Module.onRuntimeInitialized = () => {

  const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

};

Затем запустите приведенный выше код с помощью следующей команды:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js

Приведенный выше код объясняется следующим образом:

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

  • cwrapиспользуется в экспортированном файле C (fftools/ffmpeg.c)изproxy_mainИспользуя, сигнатура функцииint main(int argc, char **argv)intСоответствует JavaScriptnumber, argc представляет количество аргументов иchar **argvэто указатель в C, представляющий массив указателей на фактические параметры, которые также могут быть сопоставлены сnumber

  • Затем обработайтеffmpegЛогика совместимости с передачей аргументов для запуска в командной строкеffmpeg -hide_banner, в нашем коде через вызов функции нужноmain(2, ["./ffmpeg", "-hide_banner"]), первый параметр хорошо разрешен, так как же нам передать массив строк? Эту проблему можно разбить на две части:

    • Нам нужно преобразовать строку javascript в массив символов в C
    • Нам нужно преобразовать массив в JavaScript в массив указателей в C

Первая часть проста, потому что Emscripten предоставляет вспомогательную функцию.writeAsciiToMemoryдля выполнения этой работы:

const str = "FFmpeg.wasm";

const buf = Module._malloc(str.length + 1); // 额外分配一个字节的空间来存放 0 表示字符串的结束

Module.writeAsciiToMemory(str, buf);

Вторая часть немного сложнее, нам нужно создать массив указателей на 32-битные целые числа в C, что можно сделать с помощьюsetValueЧтобы помочь нам создать этот массив:

const ptrs = [123, 3455];

const buf = Module._malloc(ptrs.length * Uint32Array.BYTES_PER_ELEMENT);

ptrs.forEach((p, idx) => {

  Module.setValue(buf + (Uint32Array.BYTES_PER_ELEMENT * idx), p, 'i32');

});

Комбинируя приведенный выше код, мы можем получитьffmpegИнтерактивная программа:

const Module = require('./dist/ffmpeg-core');



Module.onRuntimeInitialized = () => {

  const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

  const args = ['ffmpeg', '-hide_banner'];

  const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);

  args.forEach((s, idx) => {

    const buf = Module._malloc(s.length + 1);

    Module.writeAsciiToMemory(s, buf);

    Module.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');

  })

  ffmpeg(args.length, argsPtr);

};

Затем запустите программу той же командой:

node --experimental-wasm-threads --experimental-wasm-bulk-memory ffmpeg.js

Результат вышеуказанного запуска выглядит следующим образом:

Вы можете видеть, что мы успешно скомпилировали и запустили ffmpeg 🎉.

Работа с файловой системой Emscripten

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

Нажмите здесь, чтобы узнать больше оAPI файловой системы.

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

мы пройдемfs.readFileSync()Метод читается с именемflame.aviвидеофайл, затем используйтеFS.writeFile()Запишите его в файловую систему Emscripten.

const fs = require('fs');

const Module = require('./dist/ffmpeg-core');



Module.onRuntimeInitialized = () => {

  const data = Uint8Array.from(fs.readFileSync('./flame.avi'));

  Module.FS.writeFile('flame.avi', data);



  const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

  const args = ['ffmpeg', '-hide_banner'];

  const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);

  args.forEach((s, idx) => {

    const buf = Module._malloc(s.length + 1);

    Module.writeAsciiToMemory(s, buf);

    Module.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');

  })

  ffmpeg(args.length, argsPtr);

};

Скомпилируйте видео с помощью ffmpeg.wasm

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

Модифицируем код следующим образом:

const fs = require('fs');

const Module = require('./dist/ffmpeg-core');



Module.onRuntimeInitialized = () => {

  const data = Uint8Array.from(fs.readFileSync('./flame.avi'));

  Module.FS.writeFile('flame.avi', data);



  const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

  const args = ['ffmpeg', '-hide_banner', '-report', '-i', 'flame.avi', 'flame.mp4'];

  const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);

  args.forEach((s, idx) => {

    const buf = Module._malloc(s.length + 1);

    Module.writeAsciiToMemory(s, buf);

    Module.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');

  });

  ffmpeg(args.length, argsPtr);



  const timer = setInterval(() => {

    const logFileName = Module.FS.readdir('.').find(name => name.endsWith('.log'));

    if (typeof logFileName !== 'undefined') {

      const log = String.fromCharCode.apply(null, Module.FS.readFile(logFileName));

      if (log.includes("frames successfully decoded")) {

        clearInterval(timer);

        const output = Module.FS.readFile('flame.mp4');

        fs.writeFileSync('flame.mp4', output);

      }

    }

  }, 500);



};

В приведенном выше коде мы добавляем таймер, потому что процесс транскодирования видео ffmpeg асинхронный, поэтому нам нужно постоянно читать, есть ли флаг перекодированного файла в файловой системе Emscripten, когда мы получаем флаг файла и не является неопределенным, мы используемModule.FS.readFile()Метод считывает перекодированный видеофайл из файловой системы Emscripten, а затем передаетfs.writeFileSync()Запишите видео в локальную файловую систему. В итоге получим следующий результат:

Перекодируйте видео с помощью ffmpeg и воспроизведите его в браузере.

На предыдущем шаге мы успешно использовали скомпилированный ffmpeg на стороне Node для завершенияaviотформатировать вmp4Перекодирование формата, далее мы будем использовать ffmpeg для перекодирования видео в браузере и воспроизведения его в браузере.

Хотя ffmpeg, который мы скомпилировали ранее, можетaviтранскодирование формата вmp4, но это перекодирование через формат кодирования по умолчаниюmp4Файл нельзя воспроизвести прямо в браузере, потому что браузер не поддерживает эту кодировку, поэтому нам нужно использоватьlibx264кодировщик будетmp4Файл закодирован в формат кодирования, воспроизводимый браузером.

первый вWebAssemblyСкачать под каталогомx264Исходный код кодировщика:

curl -OL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-20170226-2245-stable.tar.bz2

tar xvfj x264-snapshot-20170226-2245-stable.tar.bz2

Затем перейдите в папку x264, вы можете создатьbuild-x264.shфайл и добавьте следующее:

 #!/bin/bash -x



ROOT=$PWD

BUILD_DIR=$ROOT/build



cd $ROOT/x264-snapshot-20170226-2245-stable

ARGS=(

  --prefix=$BUILD_DIR

  --host=i686-gnu                     # use i686 gnu

  --enable-static                     # enable building static library

  --disable-cli                       # disable cli tools

  --disable-asm                       # disable asm optimization

  --extra-cflags="-s USE_PTHREADS=1"  # pass this flags for using pthreads

)

emconfigure ./configure "${ARGS[@]}"



emmake make install-lib-static -j4



cd -

Обратите внимание, что вам нужно запустить следующие команды в каталоге WebAssembly для сборки x264:

bash x264-snapshot-20170226-2245-stable/build-x264.sh

установленыx264После энкодера его можно добавить в скрипт компиляции ffmpeg для открытияx264переключатель, на этот раз мы вffmpegСоздайте Bash-скрипт в папке для сборки, создайтеbuild.shследующим образом:

 #!/bin/bash -x



emcc -v



ROOT=$PWD

BUILD_DIR=$ROOT/build



cd $ROOT/FFmpeg



CFLAGS="-s USE_PTHREADS -I$BUILD_DIR/include"

LDFLAGS="$CFLAGS -L$BUILD_DIR/lib -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB



CONFIG_ARGS=(

 --target-os=none        # use none to prevent any os specific configurations

 --arch=x86_32           # use x86_32 to achieve minimal architectural optimization

 --enable-cross-compile  # enable cross compile

 --disable-x86asm        # disable x86 asm

 --disable-inline-asm    # disable inline asm

 --disable-stripping

 --disable-programs      # disable programs build (incl. ffplay, ffprobe & ffmpeg)

 --disable-doc           # disable doc

 --enable-gpl            ## required by x264

 --enable-libx264        ## enable x264

 --extra-cflags="$CFLAGS"

 --extra-cxxflags="$CFLAGS"

 --extra-ldflags="$LDFLAGS"

 --nm="llvm-nm"

 --ar=emar

 --ranlib=emranlib

 --cc=emcc

 --cxx=em++

 --objcc=emcc

 --dep-cc=emcc

 )



emconfigure ./configure "${CONFIG_ARGS[@]}"



 # build ffmpeg.wasm

emmake make -j4



cd -

Для приведенного выше сценария компиляции выполните следующие команды в каталоге WebAssembly, чтобы обработать файл конфигурации и скомпилировать файл:

bash FFmpeg/build.sh

Затем создайте файл сценария для настройки выходного файла сборки.build-with-emcc.sh:

ROOT=$PWD

BUILD_DIR=$ROOT/build



cd FFmpeg



ARGS=(

  -I. -I./fftools -I$BUILD_DIR/include

  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib

  -Qunused-arguments

  # 这一行加入 -lpostproc 和 -lx264,添加加入 x264 的编译

  -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c

  -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread

  -O3                                           # Optimize code with performance first

  -s USE_SDL=2                                  # use SDL2

  -s USE_PTHREADS=1                             # enable pthreads support

  -s PROXY_TO_PTHREAD=1                         # detach main() from browser/UI main thread

  -s INVOKE_RUN=0                               # not to run the main() in the beginning

  -s EXPORTED_FUNCTIONS="[_main, _proxy_main]"  # export main and proxy_main funcs

  -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"   # export preamble funcs

  -s INITIAL_MEMORY=268435456                    # 268435456 bytes = 268435456 MB

)

emcc "${ARGS[@]}"



cd -

Затем запустите этот скрипт, получите пошагово скомпилированный объектный файл, скомпилируйте в WASM и JS связующий код:

bash FFmpeg/build-with-emcc.sh

Фактическое перекодирование с помощью ffmpeg

Мы создадим веб-страницу, затем предоставим кнопку для загрузки видеофайла и воспроизведем загруженный видеофайл. Хотя видеофайлы в формате avi нельзя воспроизвести непосредственно в Интернете, мы можем воспроизвести их после перекодирования с помощью ffmpeg.

в каталоге ffmpegwasmсоздать папкуindex.htmlфайл, затем добавьте следующее:

<html>                                                                                                                                            

  <head>                                                                                                                                          

    <style>                                                                                                                                       

      html, body {                                                       

        margin: 0;                                                       

        width: 100%;                                                     

        height: 100%                                                     

      }                                                                  

      body {                                                                                                                                      

        display: flex;                                                   

        flex-direction: column;

        align-items: center;                                             

      }   

    </style>                                                                                                                                      

  </head>                                                                

  <body>                                                                 

    <h3>上传视频文件,然后转码到 mp4 (x264) 进行播放!</h3>

    <video id="output-video" controls></video><br/> 

    <input type="file" id="uploader">                   

    <p id="message">ffmpeg 脚本需要等待 5S 左右加载完成</p>

    <script type="text/javascript">                                                                                                               

      const readFromBlobOrFile = (blob) => (

        new Promise((resolve, reject) => {

          const fileReader = new FileReader();

          fileReader.onload = () => {

            resolve(fileReader.result);

          };

          fileReader.onerror = ({ target: { error: { code } } }) => {

            reject(Error(`File could not be read! Code=${code}`));

          };

          fileReader.readAsArrayBuffer(blob);

        })

      );

      

      const message = document.getElementById('message');

      const transcode = async ({ target: { files } }) => {

        const { name } = files[0];

        message.innerHTML = '将文件写入到 Emscripten 文件系统';

        const data = await readFromBlobOrFile(files[0]);                                                                                          

        Module.FS.writeFile(name, new Uint8Array(data));                                                                                          

        const ffmpeg = Module.cwrap('proxy_main', 'number', ['number', 'number']);

        const args = ['ffmpeg', '-hide_banner', '-nostdin', '-report', '-i', name, 'out.mp4'];

        

        const argsPtr = Module._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);

        args.forEach((s, idx) => {                                       

          const buf = Module._malloc(s.length + 1);                      

          Module.writeAsciiToMemory(s, buf);                                                                                                      

          Module.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');

        });                   

         

        message.innerHTML = '开始转码';                        

        ffmpeg(args.length, argsPtr);

                                                           

        const timer = setInterval(() => {               

          const logFileName = Module.FS.readdir('.').find(name => name.endsWith('.log'));

          if (typeof logFileName !== 'undefined') {                                                                                               

            const log = String.fromCharCode.apply(null, Module.FS.readFile(logFileName));

            if (log.includes("frames successfully decoded")) {

              clearInterval(timer);                                      

              message.innerHTML = '完成转码';

              const out = Module.FS.readFile('out.mp4');

              const video = document.getElementById('output-video');

              video.src = URL.createObjectURL(new Blob([out.buffer], { type: 'video/mp4' }));

            }                                                            

          } 

        }, 500);                                                         

      };  

      document.getElementById('uploader').addEventListener('change', transcode);

    </script>                                                            

    <script type="text/javascript" src="./dist/ffmpeg-core.js"></script>

  </body>                         

</html>           

Откройте указанную выше веб-страницу и запустите.

Как отлаживать код WebAssembly?

Оригинальный способ отладки WebAssembly

Инструменты разработчика Chrome в настоящее время поддерживают отладку WebAssembly. Несмотря на некоторые ограничения, анализ одной инструкции и исходные трассировки стека можно анализировать для текстовых файлов WebAssembly, как показано на следующем рисунке:

Описанный выше подход хорошо работает для некоторых модулей WebAssembly без каких-либо других зависимостей, поскольку эти модули требуют лишь небольшого объема отладки. Однако для сложных приложений, таких как сложные приложения, написанные на C/C++, когда один модуль зависит от многих других модулей, а исходный код сильно отличается от отображения скомпилированного текстового формата WebAssembly, описанный выше метод отладки не работает. Он настолько интуитивно понятен, что вы можете только догадываться, как в нем работает код, и большинству людей сложно понять сложный ассемблерный код.

Более интуитивно понятный способ отладки

Современные проекты JavaScript обычно имеют процесс компиляции во время разработки. Используйте ES6 для разработки и компилируйте в ES5 и ниже для запуска. Если вам нужно отладить код в это время, это включает концепцию исходной карты, которая используется для сопоставления. Расположение скомпилированного соответствующего кода в исходном коде, исходной карте делает клиентский код более читабельным и удобным для отладки, но не окажет большого влияния на производительность.

Emscripten, компилятор кода C/C++ в код WebAssembly, поддерживает внедрение соответствующей отладочной информации в код во время компиляции, создает соответствующую исходную карту, а затем устанавливает код, написанный командой Chrome.C/C++ Devtools SupportРасширение для браузера, вы можете отлаживать код C/C++ с помощью Chrome DevTools.

Принцип здесь на самом деле заключается в том, что когда Emscripten компилируется, он генерирует файл отладки в формате DWARF, который является общим форматом файла отладки, используемым большинством компиляторов, иC/C++ Devtools SupportОн будет анализировать файл DWARF и предоставлять информацию, связанную с исходной картой, для Chrome Devtools при отладке, чтобы разработчики могли отлаживать код C/C++ в Chrome Devtools версии 89+.

Отладка простого приложения C

Поскольку файлы отладки в формате DWARF могут обеспечивать обработку имен переменных, печать сообщений форматированного типа, выполнение выражений в исходном коде и т. д., теперь давайте на самом деле напишем простую программу на C, затем скомпилируем ее в WebAssembly и запустим в браузере. Запустите ее, чтобы увидеть фактический эффект отладки.

Во-первых, давайте перейдем в созданный ранее каталог WebAssembly, активируем команды, связанные с emcc, а затем проверим эффект активации:

cd emsdk && source emsdk_env.sh

emcc --version # emcc (Emscripten gcc/clang-like replacement) 1.39.18 (a3beeb0d6c9825bd1757d03677e817d819949a77)

Затем создайте его в WebAssembly.tempпапку, затем создайтеtemp.cфайл, заполните следующее и сохраните:

#include <stdlib.h>



void assert_less(int x, int y) {

  if (x >= y) {

    abort();

  }

}



int main() {

  assert_less(10, 20);

  assert_less(30, 20);

}

Приведенный выше код выполняетсяasset_less, если встретишьx >= yВ этом случае будет выброшено исключение и выполнение программы будет прекращено.

Измените каталог в терминале наtempВыполнить в каталогеemccкоманда для компиляции:

emcc -g temp.c -o temp.html

Указанная выше команда добавлена ​​в обычной форме компиляции.-gпараметр, указывающий Emscripten вводить отладочную информацию DWARF в код во время компиляции.

Теперь вы можете запустить HTTP-сервер, вы можете использоватьnpx serve ., затем посетитеlocalhost:5000/temp.htmlОцените беговой эффект.

Установлены расширения Chrome:chrome.Google.com/веб-магазин/…Chrome Devtools обновлен до версии 89+.

Чтобы увидеть эффект отладки, необходимо установить некоторые параметры.

  1. Включите параметр отладки WebAssembly в Chrome Devtools.

После настройки вверху панели инструментов появится синяя кнопка Reload, нужно перезагрузить конфигурацию, просто нажмите на нее.

  1. Установите параметры отладки, чтобы приостановить работу с исключениями

  1. Обновите браузер, и вы обнаружите, что точка останова остановилась наtemp.js, связующий код JS, скомпилированный и сгенерированный Emscripten, а затем ищите его в стеке вызовов, вы можете увидетьtemp.cи найдите место, где было выбрано исключение:

Как видите, мы успешно просмотрели код C в Chrome Devtools, и код остановился наabort()В то же время вы также можете просмотреть значение в текущей области, например, когда мы отлаживаем JS:

можно посмотреть как вышеx,yзначение, наведите указатель мыши наxЗначение в это время также может отображаться на .

Просмотр значений сложного типа

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

Мы демонстрируем описанный выше эффект на примере рисования графики Мандельброта на C++, которая также создается в каталоге WebAssembly.mandelbrotпапку, затем добавьтеmandelbrot.ccФайл и заполните следующее:

#include <SDL2/SDL.h>

#include <complex>



int main() {

  // 初始化 SDL 

  int width = 600, height = 600;

  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* window;

  SDL_Renderer* renderer;

  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,

                              &renderer);



  // 为画板填充随机的颜色

  enum { MAX_ITER_COUNT = 256 };

  SDL_Color palette[MAX_ITER_COUNT];

  srand(time(0));

  for (int i = 0; i < MAX_ITER_COUNT; ++i) {

    palette[i] = {

        .r = (uint8_t)rand(),

        .g = (uint8_t)rand(),

        .b = (uint8_t)rand(),

        .a = 255,

    };

  }

  

  



  // 计算 曼德博 集合并绘制 曼德博 图形

  std::complex<double> center(0.5, 0.5);

  double scale = 4.0;

  for (int y = 0; y < height; y++) {

    for (int x = 0; x < width; x++) {

      std::complex<double> point((double)x / width, (double)y / height);

      std::complex<double> c = (point - center) * scale;

      std::complex<double> z(0, 0);

      int i = 0;

      for (; i < MAX_ITER_COUNT - 1; i++) {

        z = z * z + c;

        if (abs(z) > 2.0)

          break;

      }

      SDL_Color color = palette[i];

      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);

      SDL_RenderDrawPoint(renderer, x, y);

    }

  }





  // 将我们在 canvas 绘制的内容渲染出来

  SDL_RenderPresent(renderer);





  // SDL_Quit();

}

Приведенный выше код занимает около 50 строк, но ссылается на две стандартные библиотеки C++:SDLа такжеcomplex numbers, что немного усложняет наш код, давайте скомпилируем приведенный выше код, чтобы увидеть, как отлаживает Chrome Devtools.

При компиляции с-g, который указывает компилятору Emscripten принести отладочную информацию и попросить Emscripten внедрить библиотеку SDL2 во время компиляции и разрешить библиотеке использовать любой объем памяти во время выполнения:

emcc -g mandelbrot.cc -o mandelbrot.html \

     -s USE_SDL=2 \

     -s ALLOW_MEMORY_GROWTH=1

использовать то же самоеnpx serve .команда для запуска локального веб-сервера, а затем доступhttp://localhost:5000/mandelbrot.htmlВы можете увидеть следующие эффекты:

Откройте инструменты разработчика, затем вы можете выполнить поискmandelbrot.ccфайла мы видим следующее:

В первом цикле for мы можемpaletteКакая строка оператора присваивания попадает в точку останова, а затем обновляет веб-страницу. Мы обнаруживаем, что логика выполнения останавливается в нашей точке останова. Взглянув на панель Scope справа, мы можем увидеть интересное содержимое.

Использование панели «Область»

Мы можем видеть сложные типы, такие какcenter,palette, вы также можете развернуть их, чтобы увидеть конкретные значения в сложном типе:

Посмотреть прямо в программе

При этом подведите мышку кpaletteВ переменной вы также можете просмотреть тип значения:

использовать в консоли

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

Вы также можете выполнять операции со значениями и вычислениями для сложных типов:

Используйте функцию часов

Мы также можем использовать функцию наблюдения в панели отладки, добавить i в цикле for в список наблюдения, а затем возобновить выполнение программы, чтобы увидеть изменение i:

Более сложная пошаговая отладка

Мы также можем использовать несколько других инструментов отладки: step over, step in, step out, step и т. д. Например, мы используем step over, чтобы выполнить два шага назад:

Вы можете просмотреть значение переменной текущего шага или соответствующее значение на панели Scope.

Отладка сторонних библиотек, скомпилированных без исходного кода

Раньше мы только компилировалиmandelbrot.ccфайл, и попросите Emscripten предоставить нам встроенные библиотеки, связанные с SDL, при компиляции.Поскольку библиотека SDL не скомпилирована из исходного кода, она не принесет информацию, связанную с отладкой, поэтому мы толькоmandelbrot.ccЕго можно отлаживать, просматривая код C++, в то время как для контента, связанного с SDL, вы можете просматривать только код, связанный с WebAssembly, для отладки.

Например, мы устанавливаем точку останова при вызове SDL_SetRenderDrawColor в строке 41 и используем шаг для входа в функцию:

примет следующий вид:

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

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

Новая стратегия генерации имен

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

Новая стратегия именования относится к стратегиям именования других инструментов дизассемблирования, используяСтратегия именования WebAssemblyЧасть содержимого, содержимое, связанное с импортом/экспортом пути, вы можете видеть, что мы можем отображать информацию, связанную с именем функции, для функций в нашей текущей панели отладки:

Даже если возникает ошибка программы, на основе типа и индекса оператора может быть сгенерировано что-то вроде$func123Такое имя значительно улучшает трассировку стека и дизассемблирование.

Посмотреть панель памяти

Если вы хотите отладить содержимое, связанное с памятью, занятое программой в это время, вы можете просмотреть панель Scope в контексте WebAssembly.Module.memories.$env.memory, но он может видеть только некоторые независимые байты и не может понимать другие форматы данных, соответствующие этим байтам, например формат ASCII. Но Chrome DevTools также дает нам некоторые другие более мощные формы просмотра памяти, когда мы щелкаем правой кнопкой мышиenv.memory, вы можете выбрать панель Reveal in Memory Inspector:

или нажмитеenv.memoryМаленький значок рядом с:

Панель памяти можно открыть:

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

Профилирование кода WebAssembly

Поскольку мы вводим много отладочной информации в код во время компиляции, код для запуска является неоптимизированным и многословным кодом, поэтому время выполнения будет медленным, поэтому, если вы хотите оценить производительность работающей программы, вы не можете использоватьperformance.nowилиconsole.timeИ т. Д. API, потому что число, связанные с производительностью, полученные этими функциями, обычно не отражают эффекты реального мирового.

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

Можно видеть, что приведенные выше типичные моменты времени, такие как 161 мс или 461 мс для LCP и FCP, являются показателями производительности, которые могут отражать реальный мир.

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

Отладка на другой машине

При сборке на Docker, виртуальных машинах или других исходных серверах вы можете столкнуться с тем, что пути к исходным файлам, используемые во время сборки, несовместимы с путями к файлам в локальной файловой системе, что может привести к тому, что инструменты разработчика будут доступны в среда выполнения во время выполнения.Файл отображается на панели «Источники», но содержимое файла не может быть загружено.

Чтобы решить эту проблему, нам нужно установитьC/C++ Devtools SupportУстановите отображение пути в конфигурации, нажмите «Параметры» расширения:

Затем добавьте сопоставление путей, заполните путь к предыдущей сборке исходного файла в old/path и заполните путь к файлу, существующему в настоящее время в локальной файловой системе, в new/path:

Приведенные выше сопоставленные функции и некоторые отладчики C++, такие как GDBset substitute-pathи LLDBtarget.source-mapТак же, как. Таким образом, когда инструмент разработчика ищет исходный файл, он проверяет, есть ли соответствующее сопоставление в настроенном сопоставлении пути.Если исходный путь не может загрузить файл, инструмент разработчика попытается загрузить файл из сопоставления path, иначе загрузка не будет выполнена.

Отладка оптимизированного кода сборки

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

В настоящее время инструменты разработчика могут поддерживать отладку большей части оптимизированного кода, в дополнение к отсутствию хорошей поддержки встраивания функций.Чтобы уменьшить влияние отладки, вызванное отсутствием поддержки встраивания функций, рекомендуется скомпилировать код присоединиться, когда-fno-inlineфлаг для деоптимизации времени сборки (обычно с-Oпараметр), чтобы встроить функцию, инструменты разработчика исправят эту проблему в будущем. Таким образом, сценарий компиляции для простой программы на C, упомянутой ранее, выглядит следующим образом:

emcc -g temp.c -o temp.html \

     -O3 -fno-inline

Хранить отладочную информацию отдельно

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

Чтобы ускорить компиляцию и загрузку модулей WebAssembly, вы можете разделить отладочную информацию на отдельные файлы WebAssembly во время компиляции, а затем загрузить их по отдельности.-gseparate-dwarfработать:

emcc -g temp.c -o temp.html \

     -gseparate-dwarf=temp.debug.wasm

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

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

emcc -g temp.c -o temp.html \

     -O3 -fno-inline \

     -gseparate-dwarf=temp.debug.wasm \

     -s SEPARATE_DWARF_URL=file://[temp.debug.wasm 在本地文件系统的存储地址]

Отладка кода ffmpeg в браузере

Благодаря этой статье мы получили глубокое понимание того, как отлаживать код C/C++, созданный с помощью Emscripten, в браузере.Вышеприведенный пример объясняет распространенный пример без зависимостей и пример, который зависит от SDL стандартной библиотеки C++, а также объясняет текущую stage Вещи и ограничения, которые могут делать инструменты отладки, а затем мы узнаем, как отлаживать код, связанный с ffmpeg, в браузере с помощью полученных знаний.

Построить с отладочной информацией

Нам просто нужно изменить скрипт сборки, упомянутый в предыдущей статье.build-with-emcc.sh,Присоединяйся-gСоответствующие флаги:

ROOT=$PWD

BUILD_DIR=$ROOT/build





cd ffmpeg-4.3.2-3





ARGS=(

  -g # 在这里添加,告诉编译器需要添加调试

  -I. -I./fftools -I$BUILD_DIR/include

  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib

  -Qunused-arguments

  -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c

  -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread

  -O3                                           # Optimize code with performance first

  -s USE_SDL=2                                  # use SDL2

  -s USE_PTHREADS=1                             # enable pthreads support

  -s PROXY_TO_PTHREAD=1                         # detach main() from browser/UI main thread

  -s INVOKE_RUN=0                               # not to run the main() in the beginning

  -s EXPORTED_FUNCTIONS="[_main, _proxy_main]"  # export main and proxy_main funcs

  -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"   # export preamble funcs

  -s INITIAL_MEMORY=268435456                    # 268435456 bytes = 268435456 MB

)

emcc "${ARGS[@]}"





cd -

Затем выполните другие операции с этим и, наконец, передайтеnode server.jsЗапускаем наш скрипт, затем открываемhttp://localhost:8080/Просмотрите эффект следующим образом:

Как видите, мы можем искать встроенный источник на панели «Источники».ffmpeg.cфайл, мы можем работать со строкой 4865 в циклеnb_outputДостигните точки останова, когда:

Затем загрузитеaviотформатировать видео, то программа сделает паузу в точке останова:

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

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

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

О будущем WebAssembly

В этой статье перечислены только некоторые из текущих основных сценариев приложений WebAssembly, включая высокую производительность, малый вес и кросс-платформенность WebAssembly, что позволяет нам запускать такие языки, как C/C++, в Интернете и настольные приложения в Интернете. Веб-контейнер.

Но чего эта статья не касается, так этоWASI, стандартизированный системный интерфейс для запуска WebAssembly в любой системе.По мере того, как производительность WebAssembly постепенно увеличивается, WASI может предоставить реально осуществимый способ запуска произвольного кода на любой платформе, как это делает Docker, но не должен ограничиваться операционной системой. Как сказал основатель Docker:

«Если бы WASM+WASI существовали в 2008 году, не было бы необходимости создавать Docker, WASM на сервере — это будущее вычислений, долгожданный стандартизированный системный интерфейс.

Еще одним интересным контентом является среда разработки WASM на стороне клиента, такая какyew, в будущем может стать таким же популярным, как React/Vue/Angular.

И инструмент управления пакетами WASMWAPM, благодаря кроссплатформенному характеру WASM, может стать предпочтительным способом обмена пакетами между различными платформами на разных языках.

В то же время, WebAssembly также является проектом, в основном разработанным W3C, спонсируемым и совместно поддерживаемым крупными производителями, включая Microsoft, Google, Mozilla и т. д. Я верю, что у WebAssembly очень многообещающее будущее.

Q & A

Вопросы и ответы...

  • Как компилировать сложные проекты CMake в WebAssembly?
  • Как изучить общий набор рекомендаций по компиляции сложных проектов CMake в WebAssembly?
  • Как совместить с проектом CMake для отладки?

вопрос:

  • Объем кода после компиляции

Ссылка на ссылку

❤️Спасибо за поддержку

Выше приведено все содержание этого обмена, я надеюсь, что это поможет вам ^_^

Не забудьте, если вам нравитсяПоделиться, Нравится, ИзбранноеТри последовательных о~.

Добро пожаловать на общедоступный номерКоманда ELabСоберите хороший товар с большой фабрики~

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

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

Код школы/социального набора ByteDance: 6466HRE

Ссылка на доставку:job.toutiao.com/s/LdnSw2C