Emscripten 및 npm

WebAssembly를 이 설정에 통합하려면 어떻게 해야 하나요? 이 도움말에서는 C/C++ 및 Emscripten을 예로 들어 이 문제를 해결해 보겠습니다.

WebAssembly (Wasm)는 이는 성능 기본 요소 또는 기존 C++를 실행하는 방법으로 프레이밍하는 것입니다. 배포될 수 있습니다 squoosh.app으로 최소한 세 번째 관점은 있습니다. 다른 프로그래밍 언어의 생태계를 활용할 수 있습니다 다음으로 바꿉니다. Emscripten: C/C++ 코드를 사용할 수 있습니다. Rust에는 wasm 지원이 내장되어 있고 Go Google팀에서도 노력하고 있습니다. 나는 다른 많은 언어도 따라올 것입니다.

이 시나리오에서 wasm은 앱의 핵심이 아니라 퍼즐입니다. 또 다른 모듈입니다. 앱에는 이미 JavaScript, CSS, 이미지 확장 소재, 심지어 React와 같은 프레임워크까지도 아우를 수 있습니다. CANNOT TRANSLATE 이 설정에 WebAssembly를 통합하시겠습니까? 이 도움말에서는 C/C++ 및 Emscripten을 예로 들어 보겠습니다.

Docker

Emscripten과 작업할 때 Docker가 매우 유용하다는 사실을 알게 되었습니다. C/C++ 라이브러리는 종종 그 기반이 되는 운영 체제와 함께 작동하도록 작성됩니다. 일관된 환경을 갖추는 것은 매우 유용합니다. Docker를 사용하면 가상화된 Linux 시스템으로 이미 Emscripten과 호환되도록 설정되어 있고 모든 도구와 종속 항목이 설치되어 있습니다 누락된 것이 있는 경우 그것이 자신의 컴퓨터나 컴퓨터에 어떤 영향을 미치는지 걱정할 필요 없이 다른 프로젝트도 있을 수 있습니다 문제가 발생하면 컨테이너를 폐기하고 있습니다. 일단 작동하면 계속 작동하고 동일한 결과를 생성합니다.

Docker 레지스트리에는 Emscripten 이미지(제공: ) trzeci 버전까지 사용할 수 있습니다.

npm과 통합

대부분의 경우 웹 프로젝트의 진입점은 npm입니다. package.json 규칙에 따라 대부분의 프로젝트는 npm install && npm run build로 빌드할 수 있습니다.

일반적으로 Emscripten에서 생성된 빌드 아티팩트 (.js.wasm)는 파일)은 또 다른 JavaScript 모듈로 취급되어야 하며 저작물. JavaScript 파일은 웹팩 또는 롤업과 같은 번들러에서 처리할 수 있지만 wasm 파일은 이미지

따라서 Emscripten 빌드 아티팩트는 빌드 프로세스의 시작:

{
    "name": "my-worldchanging-project",
    "scripts": {
    "build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
    "build:app": "<the old build command>",
    "build": "npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

새로운 build:emscripten 작업은 Emscripten을 직접 호출할 수 있지만 Docker를 사용하여 빌드 환경이 일관성이 있어야 합니다.

docker run ... trzeci/emscripten ./build.sh는 Docker가 새 기기를 가동하도록 지시합니다. trzeci/emscripten 이미지를 사용하여 컨테이너를 빌드하고 ./build.sh 명령어를 실행합니다. build.sh는 다음에 작성할 셸 스크립트입니다. --rm 알림 실행이 완료된 컨테이너를 삭제하는 Docker를 실행합니다. 이렇게 하면 비활성 머신 이미지 컬렉션을 만드는 것입니다 -v $(pwd):/src의 의미는 다음과 같습니다. Docker 이미지를 '미러링'하고 현재 디렉터리 ($(pwd))를 내부의 /src로 이동 합니다 내부의 /src 디렉터리에 있는 파일을 변경하면 실제 프로젝트에 미러링됩니다 미러링된 디렉터리는 "바인드 마운트"라고 합니다

build.sh를 살펴보겠습니다.

#!/bin/bash

set -e

export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    src/my-module.cpp

    # Create output folder
    mkdir -p dist
    # Move artifacts
    mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="

여기는 분석해야 할 것이 많아!

set -e는 셸을 '빠른 실패'로 전환합니다. 있습니다. 스크립트의 명령어가 오류가 반환되면 전체 스크립트가 즉시 중단됩니다. 이는 스크립트의 마지막 출력은 항상 성공하기 때문에 매우 유용합니다. 오류 메시지 또는 빌드 실패의 원인이 된 오류를 해결할 수 있습니다.

export 문을 사용하여 몇 가지 환경의 값을 정의합니다. 변수로 사용할 수 있습니다. 이를 사용하면 추가 명령줄 매개변수를 C 컴파일러 (CFLAGS), C++ 컴파일러 (CXXFLAGS), 링커 (LDFLAGS)가 있습니다. 모두 OPTIMIZE를 통해 옵티마이저 설정을 수신하여 모든 것이 동일한 방식으로 최적화됩니다. 몇 가지 가능한 값이 있습니다. OPTIMIZE 변수의 경우 다음과 같습니다.

  • -O0: 최적화를 수행하지 않습니다. 데드 코드가 제거되지 않았으며, Emscripten 는 내보내는 JavaScript 코드도 축소하지 않습니다. 디버깅에 적합합니다.
  • -O3: 성능을 위해 적극적으로 최적화합니다.
  • -Os: 보조 인스턴스로 성능과 크기를 적극적으로 최적화합니다. 있습니다.
  • -Oz: 크기를 공격적으로 최적화하고 필요한 경우 성능을 저하시킵니다.

웹의 경우 대부분 -Os을 추천합니다.

emcc 명령어에는 그 자체로 다양한 옵션이 있습니다. emcc는 "GCC 또는 clang 같은 컴파일러의 드롭인 대체"여야 합니다. 따라서 GCC에서 알 수 있는 플래그는 emcc에 의해 구현될 가능성이 가장 높으며, 있습니다. -s 플래그는 Emscripten을 구성할 수 있다는 점에서 특별합니다. 특히 그렇습니다. 사용 가능한 모든 옵션은 Emscripten의 settings.js님, 이 파일은 꽤 부담스러울 수 있습니다. 다음은 Emscripten 플래그 목록입니다. 웹 개발자에게 가장 중요하다고 생각합니다.

  • --bind을(를) 사용하면 embind
  • -s STRICT=1는 지원 중단된 모든 빌드 옵션의 지원을 중단합니다. 이렇게 하면 코드를 빌드하도록 설계되어야 합니다.
  • -s ALLOW_MEMORY_GROWTH=1를 사용하면 다음 경우에 메모리가 자동으로 증가됩니다. 있습니다. 이 문서 작성 시점에 Emscripten은 16MB의 메모리를 할당합니다. 있습니다. 코드가 메모리 청크를 할당하면 이 옵션은 이러한 작업은 메모리가 부족할 때 전체 wasm 모듈이 실패하도록 만듭니다. 글루 코드가 총 메모리를 확장하여 합니다.
  • -s MALLOC=...는 사용할 malloc() 구현을 선택합니다. emmalloc: 특히 Emscripten을 위한 작고 빠른 malloc() 구현입니다. 이 대안은 완전한 malloc() 구현인 dlmalloc입니다. 나만 많은 작은 객체를 할당하는 경우 dlmalloc로 전환해야 함 자주 사용하거나 스레딩을 사용하려는 경우에 최적화할 수 있습니다
  • -s EXPORT_ES6=1는 JavaScript 코드를 기본 내보내기를 제공합니다. 다음 작업에는 -s MODULARIZE=1 필요 있습니다.

다음 플래그는 항상 필요한 것은 아니며 디버깅에만 유용합니다. 용도:

  • -s FILESYSTEM=0는 Emscripten과 관련된 플래그이며 C/C++ 코드에서 파일 시스템 작업을 사용할 때 파일 시스템을 에뮬레이션합니다. 또한 컴파일하는 코드에 대해 몇 가지 분석을 수행하여 파일 시스템 에뮬레이션인지 여부를 결정합니다. 하지만 분석이 잘못될 수 있으며 추가 접착제로 상당히 많은 70kB를 지불하게 됩니다. 파일 시스템 에뮬레이션을 위한 코드도 필요하지 않을 수 있습니다 -s FILESYSTEM=0를 사용하면 Emscripten에 이 코드가 포함되지 않도록 강제할 수 있습니다.
  • -g4를 사용하면 Emscripten이 .wasm 및 wasm 모듈의 소스 맵 파일을 내보냅니다. 자세한 내용은 디버깅 과정에서 Emscripten을 사용하여 섹션을 참조하세요.

이제 됐습니다. 이 설정을 테스트하기 위해 아주 작은 my-module.cpp를 만들어 보겠습니다.

    #include <emscripten/bind.h>

    using namespace emscripten;

    int say_hello() {
      printf("Hello from your wasm module\n");
      return 0;
    }

    EMSCRIPTEN_BINDINGS(my_module) {
      function("sayHello", &say_hello);
    }

그리고 index.html:

    <!doctype html>
    <title>Emscripten + npm example</title>
    Open the console to see the output from the wasm module.
    <script type="module">
    import wasmModule from "./my-module.js";

    const instance = wasmModule({
      onRuntimeInitialized() {
        instance.sayHello();
      }
    });
    </script>

(이 gist .

모든 것을 빌드하려면 다음을 실행합니다.

$ npm install
$ npm run build
$ npm run serve

localhost:8080으로 이동하면 DevTools 콘솔:

C++ 및 Emscripten을 통해 출력된 메시지를 보여주는 DevTools

C/C++ 코드를 종속 항목으로 추가

웹 앱용 C/C++ 라이브러리를 빌드하려면 코드가 다음과 같아야 합니다. 프로젝트의 일부에 해당합니다. 프로젝트의 저장소에 코드를 수동으로 추가할 수 있습니다. npm을 사용하여 이러한 종류의 종속 항목을 관리할 수도 있습니다. 내가 웹 앱에서 libvpx를 사용하려고 합니다. libvpx .webm 파일에서 사용되는 코덱인 VP8으로 이미지를 인코딩하는 C++ 라이브러리입니다. 하지만 libvpx는 npm에도 없고 package.json도 없어서 사용할 수 없습니다. npm을 사용하여 직접 설치합니다.

이 난관에서 벗어나기 위한 napa napa를 사용하면 원하는 명령줄을 저장소 URL을 node_modules 폴더에 종속 항목으로 추가합니다.

napa를 종속 항목으로 설치합니다.

$ npm install --save napa

napa를 설치 스크립트로 실행해야 합니다.

{
// ...
"scripts": {
    "install": "napa",
    // ...
},
"napa": {
    "libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}

npm install를 실행하면 napa가 libvpx GitHub 클론을 처리합니다. libvpx 이름으로 node_modules 저장소에 저장합니다.

이제 빌드 스크립트를 확장하여 libvpx를 빌드할 수 있습니다. libvpx는 configure 사용 빌드될 make입니다. 다행히 Emscripten을 사용하면 configuremake는 Emscripten의 컴파일러를 사용합니다. 이를 위해 래퍼는 명령어 emconfigureemmake:

# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
드림

C/C++ 라이브러리는 두 부분으로 나뉩니다. 헤더 (기존에는 .h 또는 .hpp 파일)을 사용하며, 이 클래스는 데이터 구조, 클래스, 상수 등을 정의합니다. 실제 라이브러리 (기존에는 .so 또는 .a 파일)를 노출합니다. 받는사람 코드에서 라이브러리의 VPX_CODEC_ABI_VERSION 상수를 사용하려면 #include 문을 사용하여 라이브러리의 헤더 파일을 포함할 수 있습니다.

#include "vpxenc.h"
#include <emscripten/bind.h>

int say_hello() {
    printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
    return 0;
}

문제는 컴파일러가 vpxenc.h를 찾아야 할 위치를 알지 못한다는 것입니다. -I 플래그의 용도는 아래와 같습니다. 이 파일은 어떤 디렉터리를 확인할 수 있습니다 또한 컴파일러에 실제 라이브러리 파일:

# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s ASSERTIONS=0 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    -I ./node_modules/libvpx \
    src/my-module.cpp \
    build-vpx/libvpx.a

# ... below is unchanged ...

지금 npm run build를 실행하면 프로세스가 새 .js를 빌드하는 것을 확인할 수 있습니다. 새 .wasm 파일이 있고 데모 페이지는 실제로 상수를 출력합니다.

DevTools
emscripten을 통해 인쇄된 libvpx의 ABI 버전을 보여 줍니다.

빌드 프로세스에 오랜 시간이 걸릴 수도 있습니다. 그 이유는 긴 빌드 시간은 다를 수 있습니다 libvpx의 경우 시간이 오래 걸립니다. 코드를 실행할 때마다 VP8 및 VP9 둘 다의 인코더와 디코더를 컴파일하고 소스 파일이 변경되지 않은 경우에도 빌드 명령에 사용될 수 있습니다. 크기가 작더라도 my-module.cpp 변경사항을 빌드하는 데 시간이 오래 걸립니다. 그것은 매우 libvpx의 빌드 아티팩트를 유지한 상태로 유지하는 것이 빌드해 보겠습니다

이를 달성하기 위한 한 가지 방법은 환경 변수를 사용하는 것입니다.

# ... above is unchanged ...
eval $@

echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...

(요점 .

eval 명령어를 사용하면 매개변수를 전달하여 환경 변수를 설정할 수 있습니다. 추가해야 합니다 다음과 같은 경우 test 명령어가 libvpx 빌드를 건너뜁니다. $SKIP_LIBVPX가 설정됩니다 (임의의 값).

이제 모듈을 컴파일할 수 있지만 libvpx 재빌드는 건너뛸 수 있습니다.

$ npm run build:emscripten -- SKIP_LIBVPX=1

빌드 환경 맞춤설정

라이브러리가 빌드에 추가 도구에 의존하는 경우가 있습니다. 이러한 종속 항목이 Docker 이미지에서 제공하는 빌드 환경에 누락된 경우 직접 추가하세요. 예를 들어 대규모 언어 모델도 doxygen을 사용한 libvpx 문서를 참조하세요. Doxygen은 사용할 수 있지만 apt를 사용하여 설치할 수 있습니다.

build.sh에서 이 작업을 하는 경우 doxygen을 호출합니다. 이는 단순히 낭비일 뿐 아니라 오프라인일 때도 프로젝트 작업에 지장을 줄 수 있습니다.

여기에서는 자체 Docker 이미지를 빌드하는 것이 좋습니다. Docker 이미지는 빌드 단계를 설명하는 Dockerfile 작성 Dockerfile은 강력한 성능의 다양한 명령어를 사용하지만, 대부분의 FROM, RUN, ADD만 사용하면 됩니다. 이 경우에는 다음과 같습니다.

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

FROM를 사용하면 시작점으로 사용할 Docker 이미지를 선언할 수 있습니다. 있습니다. trzeci/emscripten 기본 이미지를 선택했습니다. 이미 사용 중인 이미지입니다. 진행했습니다. RUN를 사용하면 Docker 명령어로 있습니다 이러한 명령어로 컨테이너에 적용되는 변경사항은 Docker 이미지를 생성합니다 Docker 이미지가 빌드되었으며 build.sh 실행 전에 사용할 수 있는 경우 package.json a를 조정해야 합니다. 비트:

{
    // ...
    "scripts": {
    "build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
    "build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
    "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

(요점 .

이렇게 하면 Docker 이미지가 빌드되지만 아직 빌드되지 않은 경우에만 빌드됩니다. 그런 다음 모든 것이 이전과 같이 실행되지만 이제 빌드 환경에 doxygen가 있습니다. libvpx의 문서가 있습니다.

결론

C/C++ 코드와 npm이 자연스럽게 적합하지 않은 것은 당연한 일이지만, 추가적인 도구와 격리를 통해 꽤 편안하게 작동하도록 합니다. 다양한 기능을 제공합니다 이 설정이 모든 프로젝트에 작동하는 것은 아니지만 필요에 따라 조정할 수 있는 적절한 시작 지점입니다. 만약 공유해 주세요

부록: Docker 이미지 레이어 활용

또 다른 해결책은 이러한 문제를 Docker와 캐싱에 대한 Docker의 스마트한 접근 방식 Docker는 Dockerfile을 단계별로 실행하고 는 각 단계의 결과에 자체 이미지를 할당합니다. 이러한 중간 이미지는 '계층'이라고도 합니다 Dockerfile의 명령어가 변경되지 않으면 Docker는 Dockerfile을 다시 빌드할 때는 해당 단계를 실제로 다시 실행하지 않습니다. 대신 이미지가 마지막으로 빌드된 시점의 레이어를 재사용합니다.

이전에는 매번 libvpx를 다시 빌드하지 않기 위해 약간의 노력을 해야 했습니다. 확인할 수 있습니다. 대신 libvpx의 빌드 지침을 이동할 수 있습니다. build.sh에서 Dockerfile로 이동하여 Docker의 캐싱을 활용합니다. 메커니즘:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen git && \
    mkdir -p /opt/libvpx/build && \
    git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
    emconfigure ../src/configure --target=generic-gnu && \
    emmake make

(요점 .

git을 수동으로 설치하고 libvpx를 클론해야 합니다. docker build를 실행할 때 바인드 마운트를 사용해야 합니다. 부작용으로는 나파.