Wasm에 C 라이브러리 첨가

C 또는 C++ 코드로만 사용할 수 있는 라이브러리를 사용하려고 할 때가 있습니다. 전통적으로 이것은 포기하는 곳입니다. 글쎄, 더 이상은 안 되겠지. EmscriptenWebAssembly (또는 Wasm)!

도구 모음

일부 기존 C 코드를 CANNOT TRANSLATE LLVM의 Wasm 백엔드 주위에 노이즈가 있었으므로 이 문제를 파헤치기 시작했어요. 동안 간단한 프로그램을 사용해 이렇게 하면 C의 표준 라이브러리를 사용하거나 파일을 여러 개 사용하면 문제가 발생할 가능성이 높습니다. 이로 인해 배운 교훈:

Emscripten은 이전에는 C-to-asm.js 컴파일러였지만 지금은 Wasm을 타겟팅하고 전환 과정에서 내부적으로 공식 LLVM 백엔드에 보냅니다. 또한 Emscripten은 C 표준 라이브러리의 Wasm 호환 구현입니다. Emscripten 사용하기 그것은 많은 숨겨진 작업이 수반됩니다. 파일 시스템을 에뮬레이트하고, 메모리 관리를 제공하고, OpenGL을 WebGL로 래핑합니다. 스스로 개발을 경험할 필요가 없는 많은 것들입니다.

팽창을 걱정하셔야 할 것 같지만, Emscripten 컴파일러가 필요하지 않은 모든 항목을 삭제합니다. 내 Wasm 모듈은 로직에 맞게 크기가 조절됩니다. Emscripten 및 WebAssembly 팀은 앞으로 더 줄어들게 될 것입니다.

Emscripten을 다운로드하려면 website를 변경할 수 없습니다. 다음 항목을 좋아한다면 시스템에 무언가를 설치하고 싶지는 않을 것입니다. 잘 관리된 웹 애플리케이션이나 사용 가능한 Docker 이미지 다음 코드를 사용하세요.

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

간단한 코드 컴파일

C에서 함수를 작성하는 거의 표준적인 예를 들어보겠습니다. n번째 피보나치 수를 계산합니다.

    #include <emscripten.h>

    EMSCRIPTEN_KEEPALIVE
    int fib(int n) {
      if(n <= 0){
        return 0;
      }
      int i, t, a = 0, b = 1;
      for (i = 1; i < n; i++) {
        t = a + b;
        a = b;
        b = t;
      }
      return b;
    }

C를 알고 있다면 함수 자체가 그리 놀랍지 않을 것입니다. 사용자가 C는 모르지만 JavaScript도 알고 있다면, 확인할 수 있습니다

emscripten.h는 Emscripten에서 제공하는 헤더 파일입니다. 우리에게 필요한 것은 EMSCRIPTEN_KEEPALIVE 매크로에 액세스할 수 있지만 훨씬 더 많은 기능을 제공합니다. 이 매크로는 함수가 표시되더라도 함수를 삭제하지 않도록 컴파일러에 지시합니다. 사용되지 않습니다. 이 매크로를 생략하면 컴파일러가 함수를 최적화하여 아무도 사용하지 않기 때문입니다.

이 모든 것을 fib.c 파일에 저장해 보겠습니다. .wasm 파일로 변환하려면 다음을 실행합니다. 다음과 같이 Emscripten의 컴파일러 명령어 emcc를 사용해야 합니다.

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

이 명령어를 분석해 보겠습니다. emcc는 Emscripten의 컴파일러입니다. fib.c는 C입니다. 파일에서 참조됩니다. 지금까지는 꽤 순조로웠습니다. -s WASM=1는 Emscripten에 Wasm 파일을 제공하도록 지시합니다. (asm.js 파일 대신) -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'는 컴파일러가 JavaScript 파일에서 사용할 수 있는 cwrap() 함수 - 이 함수에 대한 추가 정보 확인할 수 있습니다 -O3는 컴파일러에 적극적으로 최적화하라고 지시합니다. 낮은 점수를 선택할 수도 있습니다. 이로 인해 빌드 시간이 줄어들지만 결과 번들도 컴파일러가 사용되지 않는 코드를 삭제하지 않을 수 있기 때문입니다.

명령어를 실행하면 a.out.jsa.out.wasm라는 WebAssembly 파일 Wasm 파일 (또는 '모듈')에는 컴파일된 C 코드가 포함되어 있으며 상당히 작아야 합니다. 이 JavaScript 파일은 Wasm 모듈의 로드 및 초기화를 처리하고 제공할 수 있게 되었습니다. 필요한 경우 스택, 힙 및 기타 기능이 일반적으로 제공되어야 하는 모든 운영 체제에 적용됩니다. 따라서 JavaScript 파일은 19KB (~5KB gzip됨)입니다.

간단한 작업 실행

모듈을 로드하고 실행하는 가장 쉬운 방법은 생성된 JavaScript를 사용하는 것입니다. 파일에서 참조됩니다. 해당 파일을 로드하면 Module 전역 원하는 대로 사용할 수 있습니다. 사용 cwrap 매개변수 변환을 처리하는 자바스크립트 네이티브 함수를 만듭니다. 래핑된 함수를 호출할 수 있습니다. cwrap가 함수 이름, 반환 유형, 인수 유형을 이 순서대로 인수로 반환합니다.

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

만약 이 코드를 실행하고 '144' 이는 12번째 피보나치 번호입니다.

성배: C 라이브러리 컴파일

지금까지, 우리가 작성한 C 코드는 Wasm을 염두에 두고 작성되었습니다. 코어 C3C의 기존 생태계를 활용하는 것이었습니다 개발자가 웹에서 이를 사용할 수 있도록 합니다. 이러한 라이브러리는 종종 C의 표준 라이브러리, 운영 체제, 파일 시스템 및 기타 있습니다. Emscripten은 이러한 기능 대부분을 제공하지만, 제한사항이 있습니다.

원래 목표인 WebP용 인코더를 Wasm으로 컴파일하는 작업으로 돌아가 보겠습니다. 이 WebP 코덱의 소스는 C로 작성되고 GitHub와 함께 API 문서 좋은 출발점입니다.

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

간단하게 시작하기 위해 WebPGetEncoderVersion() webp.c라는 C 파일을 작성하여 JavaScript에 encode.h를 추가합니다.

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

이것은 libwebp의 소스 코드를 가져올 수 있는지 테스트하기에 좋은 간단한 프로그램입니다. 코드를 컴파일하는 데 매개변수나 복잡한 데이터 구조가 필요하지 않기 때문에 이 함수를 호출합니다.

이 프로그램을 컴파일하려면 컴파일러에 -I 플래그를 사용하여 libwebp의 헤더 파일을 가져오고 여기에 libwebp에 추가해야 합니다. 솔직히 말해서 모든 C 되었는 지, 그것은 우리가 찾고있는 모든 것들을 제거하는 데 컴파일러에 의존했습니다. 필요하지 않습니다. 훌륭하게 작동하는 것 같았습니다.

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

이제 몇 개의 HTML과 JavaScript만 있으면 새 모듈을 로드할 수 있습니다.

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

수정 버전 번호는 출력:

올바른 버전을 보여주는 DevTools 콘솔의 스크린샷
있습니다.

JavaScript에서 Wasm으로 이미지 가져오기

인코더의 버전 번호를 얻는 것도 좋지만, 실제 더 인상적일 것입니다. 그렇죠? 그럼 해 봅시다.

가장 먼저 해야 할 일은 이미지를 와즘 랜드로 가져오는 것입니다. 이 인코딩 API가 있을 경우 이는 RGB, RGBA, BGR 또는 BGRA의 바이트 배열입니다. 다행히도 캔버스 API에는 getImageData()님, 이를 통해 Uint8ClampedArray 를 사용합니다.

async function loadImage(src) {
  // Load image
  const imgBlob = await fetch(src).then((resp) => resp.blob());
  const img = await createImageBitmap(imgBlob);
  // Make canvas same size as image
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // Draw image onto canvas
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  return ctx.getImageData(0, 0, img.width, img.height);
}

이제는 "유일한" JavaScript 랜딩의 데이터를 Wasm으로 복사하는 것이었습니다. 있습니다. 이를 위해 두 개의 추가 함수를 노출해야 합니다. Kubernetes는 Wasm 랜드 내부의 이미지에 대한 메모리와 이를 다시 해제하는 메모리가 있습니다.

    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 이미지용 버퍼를 할당하므로 픽셀당 4바이트입니다. malloc()에서 반환된 포인터는 첫 번째 메모리 셀의 주소입니다. 생성합니다. 포인터가 JavaScript 랜딩으로 반환되면 숫자일 뿐이죠. cwrap를 사용하여 함수를 JavaScript에 노출한 후에는 다음을 수행할 수 있습니다. 그 번호를 사용하여 버퍼의 시작을 찾고 이미지 데이터를 복사합니다.

const api = {
  version: Module.cwrap('version', 'number', []),
  create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
  destroy_buffer: Module.cwrap('destroy_buffer', '', ['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);

그랜드 피날레: 이미지 인코딩

이제 Wasm 랜드에서 이미지를 사용할 수 있습니다. 이제 WebP 인코더를 호출하여 제대로 해 봐! 이 WebP 문서, WebPEncodeRGBA 딱 맞는 제품인 것 같습니다. 이 함수는 입력 이미지를 가리키는 포인터를 취하고 0에서 100 사이의 품질 옵션을 제공합니다. 또한 일단 WebPFree()를 사용하여 해제해야 하는 출력 버퍼입니다. WebP 이미지로 구현됩니다.

인코딩 작업의 결과는 출력 버퍼와 그 길이입니다. 왜냐하면 C의 함수는 배열을 반환 유형으로 가질 수 없습니다 (메모리를 할당하지 않는 한 정적 전역 배열을 사용했습니다. 나는 클린 C (실제로, Wasm 포인터가 32비트 와이드라는 사실에 의존하지만, 아주 간단합니다.

    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];
    }

모든 준비가 끝났으므로 이제 인코딩 함수를 호출하고 자체 JavaScript 랜드 버퍼에 넣고 이 과정에서 할당한 모든 Wasm-land 버퍼를 해제합니다.

    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);

이미지의 크기에 따라 Wasm이 입력 및 출력 이미지를 모두 수용할 만큼 메모리를 늘릴 수 없습니다.

오류를 보여주는 DevTools 콘솔의 스크린샷

다행히 이 문제의 해결 방법은 오류 메시지에 있습니다. 이제 컴파일 명령어에 -s ALLOW_MEMORY_GROWTH=1를 추가합니다.

이제 Cloud 함수가 완성되었네요. WebP 인코더를 컴파일하고 JPEG 이미지를 WebP를 사용합니다. 작동을 증명하기 위해 결과 버퍼를 blob으로 변환하고 <img> 요소에서 실행합니다.

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);

새로운 WebP 이미지의 영광을 보세요!

DevTools의 네트워크 패널과 생성된 이미지입니다.

결론

C 라이브러리가 브라우저에서 작동하도록 하기 위해 공원을 걷는 것이 아니라 전반적인 프로세스와 데이터 흐름이 작동하는 방식을 이해하면 놀라운 결과를 얻을 수 있습니다.

WebAssembly는 웹상의 새로운 처리 가능성을 열어주고 있습니다. 크런치와 게임입니다. Wasm은 모든 것을 감탄할 수 있는 만병통치약이 아니라는 점을 모든 것에 적용될 수 있지만, 이러한 병목 현상 중 하나에 도달하면 Wasm이 매우 유용한 도구입니다.

보너스 콘텐츠: 단순하고 어려운 작업 실행

생성된 JavaScript 파일을 피하려면 있습니다. 피보나치 예시로 돌아가 봅시다. 직접 로드하고 실행하려면 다음을 수행하세요.

<!DOCTYPE html>
<script>
  (async function () {
    const imports = {
      env: {
        memory: new WebAssembly.Memory({ initial: 1 }),
        STACKTOP: 0,
      },
    };
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('/a.out.wasm'),
      imports,
    );
    console.log(instance.exports._fib(12));
  })();
</script>

Emscripten에서 생성한 WebAssembly 모듈에 사용할 메모리가 없음 사용할 수 없습니다. Wasm 모듈을 아무것이나 하는 것입니다. imports 객체, 즉 instantiateStreaming 함수. Wasm 모듈은 내부의 모든 항목에 액세스할 수 있습니다. imports 객체를 가져오고 그 밖의 다른 것은 없습니다. 규칙에 따라 모듈은 Emscripting으로 컴파일한 코드는 JavaScript 로드에서 몇 가지 사항을 예상합니다. 환경:

  • 먼저, env.memory가 있습니다. Wasm 모듈은 외부를 인식하지 못함 말하자면 작업할 메모리가 필요합니다. 입력 WebAssembly.Memory 이는 선택적으로 확장 가능한 선형 메모리 조각을 나타냅니다. 크기 조정 매개변수는 'WebAssembly 페이지 단위'에 있습니다. 즉, 위의 코드는 1페이지의 메모리를 할당하며 각 페이지의 크기는 64입니다. KiB. maximum를 제공하지 않음 메모리 증가는 이론적으로 무한합니다 (Chrome에서는 현재 2GB로 엄격하게 제한). 대부분의 WebAssembly 모듈은 있습니다.
  • env.STACKTOP는 스택이 증가하기 시작할 위치를 정의합니다. 스택 함수를 호출하고 로컬 변수에 사용할 메모리를 할당하는 데 필요합니다. 작은 코드에서는 동적 메모리 관리 문제를 피보나치 프로그램을 사용하면 전체 메모리를 스택으로 사용할 수 있으므로 STACKTOP = 0