Fetch API

Emscripten の Fetch API を使用すると、ネイティブコードはリモートサーバーから XHR(HTTP GET、PUT、POST)を介してファイルを転送し、ダウンロードしたファイルをブラウザの IndexedDB ストレージにローカルに保存できるため、後続のページ訪問でローカルに再アクセスできます。Fetch API は複数のスレッドから呼び出すことができ、ネットワークリクエストは必要に応じて同期または非同期で実行できます。

注記

Fetch API を使用するには、コードを -sFETCH でコンパイルする必要があります。

はじめに

Fetch API の使用は、例で簡単に説明できます。次のアプリケーションは、Web サーバーからファイルを非同期的にアプリケーションヒープ内のメモリにダウンロードします。

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

上記の例のように、emscripten_fetch への呼び出しに相対パス名を指定した場合、XHR は現在のページの href(URL)を基準にして実行されます。完全修飾された絶対 URL を渡すと、ドメインを跨いでのファイルのダウンロードが可能になりますが、これらは HTTP アクセス制御 (CORS) ルール の対象となります。

デフォルトでは、Fetch API は非同期的に実行されます。つまり、emscripten_fetch() 関数の呼び出しはすぐに返され、操作はバックグラウンドで継続されます。操作が完了すると、成功または失敗のコールバックが呼び出されます。

データの永続化

Fetch API によって発行された XHR リクエストは、通常のブラウザのキャッシング動作の対象となります。これらのキャッシュは一時的なものであるため、データが一定期間キャッシュに保持される保証はありません。さらに、ファイルが比較的大きい(数メガバイト)場合、ブラウザは通常、ダウンロードをまったくキャッシュしません。

ダウンロードしたファイルの永続化をより明示的に制御するために、Fetch API はブラウザの IndexedDB API と連携します。IndexedDB API は、ページへの後続の訪問で利用可能な、大きなデータファイルをロードおよび保存できます。IndexedDB ストレージを有効にするには、fetch 属性に EMSCRIPTEN_FETCH_PERSIST_FILE フラグを渡します。

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  ...
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_PERSIST_FILE;
  ...
  emscripten_fetch(&attr, "myfile.dat");
}

完全な例については、test/fetch/test_fetch_persist.c ファイルを参照してください。

メモリからのデータバイトの永続化

アプリケーションメモリの一部のバイトを IndexedDB に永続化することが役立つ場合があります(XHR を実行する必要はありません)。これは、Emscripten Fetch API に特別な HTTP アクション動詞「EM_IDB_STORE」を Emscripten Fetch 操作に渡すことで可能です。

void success(emscripten_fetch_t *fetch) {
  printf("IDB store succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("IDB store failed.\n");
  emscripten_fetch_close(fetch);
}

void persistFileToIndexedDB(const char *outputFilename, uint8_t *data, size_t numBytes) {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_STORE");
  attr.attributes = EMSCRIPTEN_FETCH_REPLACE | EMSCRIPTEN_FETCH_PERSIST_FILE;
  attr.requestData = (char *)data;
  attr.requestDataSize = numBytes;
  attr.onsuccess = success;
  attr.onerror = failure;
  emscripten_fetch(&attr, outputFilename);
}

int main() {
  // Create data
  uint8_t *data = (uint8_t*)malloc(10240);
  srand(time(NULL));
  for(int i = 0; i < 10240; ++i) data[i] = (uint8_t)rand();

  persistFileToIndexedDB("outputfile.dat", data, 10240);
}

IndexedDB からのファイルの削除

HTTP アクション動詞「EM_IDB_DELETE」を使用して、IndexedDB からファイルをクリーンアップできます。

void success(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB succeeded.\n");
  emscripten_fetch_close(fetch);
}

void failure(emscripten_fetch_t *fetch) {
  printf("Deleting file from IDB failed.\n");
  emscripten_fetch_close(fetch);
}

int main() {
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "EM_IDB_DELETE");
  emscripten_fetch(&attr, "filename_to_delete.dat");
}

同期フェッチ

いくつかのシナリオでは、呼び出し元のスレッドで XHR リクエストまたは IndexedDB ファイル操作を同期的に実行できると便利です。これにより、アプリケーションの移植が容易になり、コールバックを渡す必要がないため、コードの流れが簡素化されます。

すべてのタイプの Emscripten Fetch API 操作(XHR、IndexedDB アクセス)は、EMSCRIPTEN_FETCH_SYNCHRONOUS フラグを渡すことで同期的に実行できます。このフラグが渡されると、呼び出し元のスレッドは、フェッチ操作が完了するまでスリープしてブロックされます。次の例を参照してください。

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS;
  emscripten_fetch_t *fetch = emscripten_fetch(&attr, "file.dat"); // Blocks here until the operation is complete.
  if (fetch->status == 200) {
    printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
    // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  } else {
    printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  }
  emscripten_fetch_close(fetch);
}

上記のコードサンプルでは、成功と失敗のコールバック関数は使用されていません。ただし、指定されている場合、emscripten_fetch() が戻る前に同期的に呼び出されます。

注記

同期 Emscripten Fetch 操作には、使用される Emscripten ビルドモード(リンカフラグ)に応じて、いくつかの制限があります。

  • **フラグなし**: 非同期 Fetch 操作のみが使用可能です。

  • --proxy-to-worker: IndexedDB とやり取りしない XHR だけを実行するフェッチでは、同期 Fetch 操作が許可されます。

  • -pthread: 同期 Fetch 操作は pthreads では使用できますが、メインスレッドでは使用できません。

  • --proxy-to-worker + -pthread: メインスレッドと pthreads の両方で同期 Fetch 操作が使用できます。

進捗状況の追跡

堅牢なフェッチ管理のために、XHR のステータスを追跡するために使用できるフィールドがいくつかあります。

新しいデータを受信するたびに、onprogress コールバックが呼び出されます。これにより、ダウンロード速度を測定し、完了までの推定時間を計算できます。さらに、emscripten_fetch_t 構造体は、リクエストの HTTP ロード状態に関する情報を提供する XHR オブジェクトフィールド readyState、status、statusText を渡します。

emscripten_fetch_attr_t オブジェクトには、転送のタイムアウト期間を指定できる timeoutMSecs フィールドがあります。さらに、非同期および待機可能なフェッチに対しては、いつでも emscripten_fetch_close() を呼び出してダウンロードを中止できます。次の例では、これらのフィールドと onprogress ハンドラを示します。

void downloadProgress(emscripten_fetch_t *fetch) {
  if (fetch->totalBytes) {
    printf("Downloading %s.. %.2f%% complete.\n", fetch->url, fetch->dataOffset * 100.0 / fetch->totalBytes);
  } else {
    printf("Downloading %s.. %lld bytes complete.\n", fetch->url, fetch->dataOffset + fetch->numBytes);
  }
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

大規模ファイルの管理

フェッチのメモリ使用戦略に特に注意する必要があります。これまでの例ではすべて、EMSCRIPTEN_FETCH_LOAD_TO_MEMORY フラグが渡されており、これにより emscripten_fetch() は、onsuccess() コールバックでダウンロードされたファイルをメモリに完全に格納します。これは、ファイル全体にすぐにアクセスする必要がある場合に便利ですが、大規模ファイルの場合、メモリ使用量の点で無駄になる可能性があります。ファイルが非常に大きい場合、アプリケーションのヒープ領域に収まらない可能性もあります。

以降のセクションでは、メモリ効率の良い方法で大規模フェッチを管理する方法について説明します。

IndexedDB への直接ダウンロード

アプリケーションがローカルアクセス用にファイルをダウンロードする必要があるが、ファイルの直後の使用を必要としない場合(たとえば、後でアクセスするために事前にデータをプリロードする場合)、EMSCRIPTEN_FETCH_LOAD_TO_MEMORY フラグをまったく渡さず、代わりに EMSCRIPTEN_FETCH_PERSIST_FILE フラグだけを渡すのが良い方法です。これにより、フェッチはファイルを IndexedDB に直接ダウンロードするため、ダウンロード完了後にメモリに一時的にファイルを格納することがなくなります。このシナリオでは、onsuccess() ハンドラはダウンロードされたファイルの合計サイズのみを報告しますが、ファイルのデータバイトは含まれません。

ストリーミングダウンロード

注記: これは現在、「moz-chunked-arraybuffer」を使用するため、Firefox でのみ機能します。

アプリケーションがファイルへのランダムシークアクセスを必要とせず、ファイルにストリーミング方式で処理できる場合、EMSCRIPTEN_FETCH_STREAM_DATA フラグを使用して、ダウンロードされたファイルのバイトを順番に処理できます。このフラグが渡されると、ダウンロードされたデータチャンクは、一貫性のあるファイルの順序で onprogress() コールバックに渡されます。例については、次のスニペットを参照してください。

void downloadProgress(emscripten_fetch_t *fetch) {
  printf("Downloading %s.. %.2f%%s complete. HTTP readyState: %d. HTTP status: %d.\n"
    "HTTP statusText: %s. Received chunk [%llu, %llu[\n",
    fetch->url, fetch->totalBytes > 0 ? (fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes : (fetch->dataOffset + fetch->numBytes),
    fetch->totalBytes > 0 ? "%" : " bytes",
    fetch->readyState, fetch->status, fetch->statusText,
    fetch->dataOffset, fetch->dataOffset + fetch->numBytes);

  // Process the partial data stream fetch->data[0] thru fetch->data[fetch->numBytes-1]
  // This buffer represents the file at offset fetch->dataOffset.
  for(size_t i = 0; i < fetch->numBytes; ++i)
    ; // Process fetch->data[i];
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_STREAM_DATA;
  attr.onsuccess = downloadSucceeded;
  attr.onprogress = downloadProgress;
  attr.onerror = downloadFailed;
  attr.timeoutMSecs = 2*60;
  emscripten_fetch(&attr, "myfile.dat");
}

この場合、onsuccess() ハンドラは最終的なファイルバッファをまったく受信しないため、メモリ使用量は最小限に抑えられます。

バイト範囲ダウンロード

大きなファイルは、バイト範囲ダウンロードを実行することで、より小さなチャンクで管理することもできます。これにより、ファイルの目的のサブ範囲のみを取得するXHRまたはIndexedDB転送が開始されます。これは、たとえば、大きなパッケージファイルに特定のシークオフセットに複数の小さなファイルが含まれている場合に、それらを個別に処理する場合に役立ちます。

#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
  printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
  // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
  emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
  printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
  emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  // Make a Range request to only fetch bytes 10 to 20
  const char* headers[] = {"Range", "bytes=10-20", NULL};
  attr.requestHeaders = headers;
  attr.onsuccess = downloadSucceeded;
  attr.onerror = downloadFailed;
  emscripten_fetch(&attr, "myfile.dat");
}

TODO ドキュメント化

Emscripten_fetch()は、ドキュメント化が必要な以下の操作もサポートしています。

  • Emscripten_fetchは、HTTP PUTを介してファイルをリモートサーバーにアップロードするために使用できます。

  • Emscripten_fetch_attr_tを使用すると、カスタムHTTPリクエストヘッダーを設定できます(例:キャッシュコントロール)。

  • Emscripten_fetch_attr_tのHTTP単純認証フィールドを記述します。

  • Emscripten_fetch_attr_tのoverriddenMimeType属性を記述します。

  • Emscripten_fetch_attr_t、Emscripten_fetch_t、および#defineの個々のフィールドのドキュメントを参照します。

  • XHRを使用せずにIndexedDBからのみ読み込む例。

  • 新しいXHRを使用してIndexedDB内の既存のファイルを上書きする方法の例。

  • –preload-fileの簡単な置き換えのために、ファイルシステム全体をIndexedDBにプリロードする方法の例。

  • コンテンツをIndexedDBにgzip圧縮して保存し、読み込み時に解凍する方法の例。

  • IndexedDBへの部分的な転送の中断と再開方法の例。