Emscripten + WebAssemblyをベースにExcelのブラウザ操作を実装

1. なぜこのホイールを作るのか?

[C++] ブラウザ側で WebAssembly を使用して Excel_wasm ファイルを開くには何を使用すればよいですか_Your Menthol's Blog - CSDN Blog ブラウザ側で WebAssembly を使用して Excel_wasm ファイルを開くには何を使用すればよいですhttps://blog.csdn.net/weixin_44305576/記事/details/125545900?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168964185516800185863561%2522%252C%2522scm%2522%253A%252220140713。1 30102334..%2522%257D&request_id=168964185516800185863561&biz_id=0&utm_medium=distribute.pc_search_result.none -task- blog-2~all~baidu_landing_v2~default-4-125545900-null-null.142%5Ev88%5Econtrol_2,239%5Ev2%5Einsert_chatgpt&utm_term=C%2B%2B%20wasm&spm=1018.2226.3001.4187初めてWAを学んだときSM、私は C++ を使用していましたが、上記のブログを見たとき、Web では xls ファイルを操作することしかできず、xlsx ファイルはサポートされていませんでしたが、この記事から、Web 上で Excel を単純に操作するという著者の目的は達成できました。この記事は初心者、Web 上の Excel の操作をより適切にサポートするためにホイールを再構築します。

2. Linux開発環境のセットアップ

Rust Wasm Linux 開発環境構築_centos インストール Rust_Yu Shanma のブログ - CSDN ブログLinux_centos インストール Rust での Rust + Wasm/Wasi 開発https://blog.csdn.net/weixin_47560078/article/details/130559636 Linux 開発環境はこちらを参照してください。

3.OpenXLSX

# 参考官网 https://github.com/troldal/OpenXLSX

1.CMakeをインストールする

# https://cmake.org/download/

# 解压
tar -zxvf cmake-3.27.0.tar.gz

# 进入目录
cd cmake-3.27.0

# 安装
./bootstrap && make -j4 && sudo make install

2. OpenXLSXをダウンロードしてコンパイルします。

# clone 代码
git clone https://github.com/troldal/OpenXLSX.git

# 进入目录
cd OpenXLSX

# 新建 build 文件夹
mkdir build

# 进入 build 文件夹
cd build

# 编译
cmake ..

# 构建
cmake --build . --target OpenXLSX --config Release

# root 权限执行安装
cmake --install .

# 自定义安装路径
# cmake --install . --prefix=/home/sam/mylib

ここに赤いエラーが表示されますが、影響はありません。実際には、OpenXLSX ライブラリがインストールされています。このエラーは、他にライブラリがないことを意味します。

3. OpenXLSX 静的ライブラリを使用する

#include <OpenXLSX.hpp>

using namespace OpenXLSX;

int main() {

    XLDocument doc;
    doc.create("Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "Hello, OpenXLSX!";

    doc.save();

    return 0;
}

ここでは便宜上、ヘッダー ファイルと静的ライブラリをプロジェクトのルート ディレクトリに置きます。

4. C++ をコンパイルして実行します。

# 安装 C/C++ 编译器
# C/C++ 编译器
yum -y install gcc
yum -y install gcc-c++

[root@localhost demo]# gcc hello.c -o hello1
[root@localhost demo]# ./hello1 
Hello,world

[root@localhost demo]# g++ hello.cpp -o hello2
[root@localhost demo]# ./hello2
Hello,world!
# 指定 C++ 17 标准,链接静态库 libOpenXLSX.a 
g++ -std=c++17 main.cpp libOpenXLSX.a -o test_open_xlsx
./test_open_xlsx

ご覧のとおり、Excel ファイルが生成され、正常に開かれています。

ヘッダー ファイル #include <OpenXLSX/OpenXLSX.hpp>を直接参照する場合は、コンパイル時に静的ライブラリの場所も指定する必要があります。

5. 添付ファイル: Windows で OpenXLSX をコンパイルして使用する

 git clone が完了したら、OpenXLSX ルート ディレクトリに移動し、新しいビルド フォルダーを作成し、コンパイルを実行します。

mkdir build
cd build
cmake ..

cmake --build . --target OpenXLSX --config Release

cmake --install .

ファイル INSTALL が見つかりませんというエラーが報告されていますが、OpenXlsx は実際にはインストールされています。このエラーのため、このライブラリはまったく必要ないため、benchmark.lib がまだコンパイルされていないことは明らかです。

# 可以通过 prefix 指定安装路径, 
cmake --install . --prefix=F:\OpenXLSX

 

VS2019 で使用するには、インクルード ディレクトリ、ライブラリ ディレクトリ、ライブラリ名を設定する必要があります。

C++ 言語標準は一部の標準ライブラリに影響を与えることに注意してください。影響しない場合は、「名前空間 std にはメンバー string_view がありません」というエラーが報告されます。 

同じコードが正常に実行されます。 

6. 添付ファイル: OpenXLSX をカプセル化し、DLL としてエクスポートします。

VS2019はDLLプロジェクトを作成します。

hpp と対応する cpp を定義します。

pch.h を含める必要があることに注意してください。

コンパイル設定、

次に、生成し、 

ご覧のとおり、関数はエクスポートされています。

このDLLはRustで使用されており、

[dependencies]
# 调用外部 dll
libloader = "0.1.4"

main.rs は dll を呼び出します。 

use cstr::cstr;
use libloader::*;
use std::{ffi::CStr,os::raw::c_char};

fn main() {
    get_libfn!("dll/mydll.dll", "println", println, (), s: &str);
    println("你好");

    get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);
    println!(" 1 + 2 = {}", add(1, 2));

    get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);
    print_hello();

    get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);
    let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };
    print!("out {}", str.to_str().unwrap());

    get_libfn!("dll/Dll1.dll", "testExcel", test_excel, usize);
    test_excel();
}

コンパイルして実行すると、

7. 添付ファイル: Rust は rlib サブライブラリを呼び出します 

[workspace]
#以子目录成员方式添加 lib
members = [
    "mydll"
]

 build.rs を記述して rlib パスを指定します。

fn main() {
    // .rlib 路径
    println!("cargo:rustc-link-search=./target/debug/");
}

rlibライブラリをビルドし、

外部クレートを使用し、

use cstr::cstr;
use libloader::*;
use std::{ffi::CStr,os::raw::c_char};
extern crate mydll;


fn main() {
    get_libfn!("dll/mydll.dll", "println", println, (), s: &str);
    println("你好");

    get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);
    println!(" 1 + 2 = {}", add(1, 2));

    get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);
    print_hello();

    get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);
    let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };
    print!("out {}", str.to_str().unwrap());

    // get_libfn!("dll/Dll1.dll", "testExcel", test_excel, usize);
    // test_excel();

    mydll::test_excel_dll();

}

コンパイルして実行すると、これも正常に実行されます。

8. 添付ファイル: Rust で外部 DLL を呼び出すための 3 つのライブラリ

# 调用外部 dll
# libloader = "0.1.4"
# libloading = "0.7.2"
# dlopen2 = "0.4"

注: Rust wasm は libc をサポートしていません。

issue 文档 https://github.com/astonbitecode/j4rs/issues/53

元のビルド ターゲットは x86_64-pc-windows-msvc であるため、wasm32-unknown-unknown と互換性がないため、rlib を使用して呼び出します。

4.エムスクリプト

参考 https://developer.mozilla.org/zh-CN/docs/WebAssembly/C_to_wasm

1. ダウンロードしてインストールします

# 指定版本 git clone  -b 3.1.0  https://github.com/juj/emsdk.git

# 最新版本
git clone https://github.com/juj/emsdk.git

cd emsdk

./emsdk install latest

./emsdk activate latest

source ./emsdk_env.sh

 発生したインストールエラーは次のとおりです。

# 安装指令替换为: 
# ./emsdk.py install latest
# ./emsdk.py activate latest

# 因为 emsdk 最终会调用 emsdk.py

権限が不十分な場合は、承認が必要です。 

最後のステップでエラーが発生しました。これは、Python のコンパイルおよびインストール時の問題です。モジュールが欠落しています。

ModuleNotFoundError: No module named '_ctypes'

 解決策は次のとおりです。

yum install libffi-devel 

# 然后重新编译 python 源码,这里使用版本 Python-3.8.8.tgz

./configure --prefix=/usr/local/python3 --with-ssl 

make
 
make install

ソフトリンクを再構築し、 

sudo rm -rf /usr/local/bin/python3
sudo ln -s /usr/local/lib/python3.8/bin/python3.8 /usr/local/bin/python3

emcc -v

 

2. emcc/em++ を使用して hello C/C++ コードを wasm にコンパイルします。

# 参考官方文档
https://emscripten.org/docs/compiling/Building-Projects.html
https://emscripten.org/docs/compiling/WebAssembly.html

2.1. 編集計画 1

wasm にコンパイルし、コードの実行に使用される HTML を生成し、wasm が Web 環境で実行するために必要なすべての「接着剤」JavaScript コードを追加します。

// hello.cpp
#include <stdio.h>

int main(int argc, char ** argv) {
  printf("Hello World\n");
}

// 编译指令
// emcc hello.cpp -s WASM=1 -o hello.html

hello.html、hello.js、hello.wasmが生成されていることがわかります。htmlファイルを直接開いて実行することはできません。Webサーバーとしてデプロイする必要があります。ここではPythonを使用しています。 Webサーバーを構築します。 

# Python 版本是 3.X
python -m http.server
# Python 版本是 2.X
python -m SimpleHTTPServer

ご覧のとおり、main 関数の hello world 出力が自動的に呼び出されています。 

2.2. 編集計画 2

wasm にコンパイルし、JavaScript と WASM のみを生成します。

emcc -o hello2.js hello.cpp -O3 -s WASM=1

ご覧のとおり、hello2.js および hello2.wasm ファイルのみが生成され、テンプレート ファイルはありません。

2.3. 補足: カスタム HTML テンプレートの使用 

カスタム HTML テンプレートを使用したい場合があります。その方法を見てみましょう。

# 在项目根目录下创建文件夹 html_template
mkdir html_template

# 在 emsdk 中搜索一个叫做 shell_minimal.html 的文件,然后复制它到刚刚创建的目录下的 
html_template文件夹

cp  /home/sam/Downloads/emsdk/upstream/emscripten/src/shell_minimal.html html_template

 コンパイルスクリプトを実行して、

emcc -o hello3.html hello.cpp -O3 -s WASM=1 --shell-file html_template/shell_minimal.html

# 我们使用了 -o hello3.html,这意味编译器将仍然输出 js 胶水代码 和 html 文件
# 我们还使用了 --shell-file html_template/shell_minimal.html,这指定了要运行的例子使用 HTML 页面模板

hello3.html ランニングエフェクト、

5. JSがC/C++でカスタマイズした関数を呼び出す 

新しい hellojs.cpp を作成します。

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

int main(int argc, char ** argv) {
    printf("Hello World\n");
}

#ifdef __cplusplus
extern "C" {
#endif

int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  printf("我的函数已被调用\n");
}

#ifdef __cplusplus
}
#endif

公式の説明は以下の通りです。

デフォルトでは、Emscripten によって生成されたコードは関数のみを呼び出しmain()、その他の関数は役に立たないコードとみなされます。関数名の前に追加すると、EMSCRIPTEN_KEEPALIVEこの問題が発生するのを防ぎます。emscripten.hライブラリを使用するには、ライブラリをインポートする必要がありますEMSCRIPTEN_KEEPALIVE

注:#ifdef C++ コードでコードを参照する場合に備えて、コードが適切に動作することを保証するコード ブロックを追加しました。C と C++ では名前マングリング ルールが異なるため、追加されたコード ブロックによって問題が発生する可能性がありますが、現時点では、C++ を使用するときにこれらのコードが外部 C 言語関数として扱われるように、この追加のコード ブロックをセットアップしています。

コンパイルスクリプトは次のとおりです。

emcc -o hellojs.html hellojs.cpp -O3 -s WASM=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" --shell-file html_template/shell_minimal.html

次に、新しいmyFunction()JavaScript 関数を実行し、ページにボタンを追加し、js イベントをバインドする必要があります。

<button class="mybutton">运行我的函数</button>

document.querySelector(".mybutton").addEventListener("click", function () {
  alert("检查控制台");
  var result = Module.ccall(
    "myFunction", // name of C function
    null, // return type
    null, // argument types
    null,
  ); // arguments
});

次にウェブを実行し、 

 6. Emscripten を使用して OpenXLSX をコンパイルする

1. 補足: emmake/emcmake

2. OpenXLSXを再コンパイルする

# 编译
emcmake cmake ..
# 生成lib.a 的llvm bitcode   
emmake make 

出力フォルダーの下に静的ライブラリが生成されます。

3. プロジェクトで静的ライブラリを使用する

静的ライブラリをプロジェクトの依存ライブラリにコピーし、コンパイルします。

// main.cpp
#include <OpenXLSX/OpenXLSX.hpp>
#include <emscripten/emscripten.h>

using namespace OpenXLSX;

int main(){
    printf("call main function default\n");
    return 0;
}

#ifdef __cplusplus
extern "C" {
#endif

int EMSCRIPTEN_KEEPALIVE test_open_xlsx() {

    XLDocument doc;
    doc.create("Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "Hello, OpenXLSX!";

    doc.save();

    printf("函数 test_open_xlsx 已被调用\n");

    return 0;
}

#ifdef __cplusplus
}
#endif
// build_sh.sh
export SOURCE=./wasm_demo

echo "Running Emscripten..."

em++ -std=c++17 -O3 -flto  ${SOURCE}/main.cpp -s WASM=1 \
-s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" -s ASSERTIONS=1 --shell-file ${SOURCE}/html_template/shell_minimal.html \
-o ${SOURCE}/final.html -I${SOURCE}/mylib/include -L${SOURCE}/mylib/lib64 \
-lOpenXLSX 

echo "Finished Build"

最後に、3 つの最終ファイルが生成されます。 

Final.html をデプロイすると、アクセス効果は次のようになります。

js は関数を呼び出します。

Cmake を直接使用すると、コンパイルされた静的ライブラリに互換性がなく、最終的に関数呼び出し時にエラーが発生します。

7. リソース IO 

プロジェクトの C++ は多くのシステム API (主に一部のファイル IO) を使用します。emscripten はファイル IO を適切にカプセル化し、さまざまな環境でのファイル IO の適応問題と互換性のある仮想ファイル システムを提供します。

最下位レベルでは、Emscripten は 3 つのファイル システムを提供します

  • MEMFS: システム データは完全にメモリに保存されます。これは webpack の実装に非常に似ています。メモリ内で一連のファイル システム操作をシミュレートします。実行時に書き込まれたファイルはローカルに保存されません。
  • NODEFS: Node.js ファイル システム。ローカル ファイル システムにアクセスしてファイルを永続的に保存できますが、Node.js 環境でのみ使用できます。
  • IDBFS: IndexDB ファイル システム。ブラウザの IndexDB オブジェクトに基づいており、永続的に保存できますが、ブラウザ環境でのみ使用されます。

7.1. src/setting.js の概要

 このファイルは、NODERAWFS、EXPORT_ES6、SIDE_MODULE など、-s <flag> で設定できる内容を示します。

 

7.2. IDBFS の使用例

公式ドキュメント、

https://emscripten.org/docs/api_reference/Filesystem-API.html#filesystem-api-idbfs

https://emscripten.org/docs/porting/files/index.html#packaging-code-index

https://emscripten.org/docs/porting/files/file_systems_overview.html#file-system-overview

main.cpp を変換し、異なるコンパイル パラメータに対応する関数をエクスポートする 2 つの方法と、js を呼び出すときの _ プレフィックスの違いに注意してください。

#include <OpenXLSX/OpenXLSX.hpp>
#include <emscripten/emscripten.h>
#include <emscripten/val.h>
#include <emscripten/bind.h> 

using namespace OpenXLSX;

int main(){
    printf("call main function default\n");
    return 0;
}


void  setup_idbfs()  {
    EM_ASM(
        FS.mkdir('/data');
        FS.mount(IDBFS, {root : '.'},'/data');
    );
}

int test_node_fs(){

    setup_idbfs();

    XLDocument doc;
    doc.create("/data/Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "EMSCRIPTEN_BINDINGS >>> Hello, OpenXLSX!";

    doc.save();

    printf("函数 test_node_fs 已被调用\n");

    return 0;

}

EMSCRIPTEN_BINDINGS(Module){
    emscripten::function("test_node_fs",&test_node_fs);
}


#ifdef __cplusplus
extern "C" {
#endif

int EMSCRIPTEN_KEEPALIVE test_open_xlsx() {

    XLDocument doc;
    doc.create("/data/Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "EMSCRIPTEN_KEEPALIVE >>> Hello, OpenXLSX!";

    doc.save();

    printf("函数 test_open_xlsx 已被调用\n");

    return 0;
}

#ifdef __cplusplus
}
#endif

 コンパイルスクリプト、

# build.sh
export SOURCE=./wasm_demo

echo "Running Emscripten..."

emcc -std=c++17 -Oz --bind ${SOURCE}/main.cpp -s WASM=1 \
--shell-file ${SOURCE}/html_template/shell_minimal.html \
-s EXTRA_EXPORTED_RUNTIME_METHODS='[FS]' -s ASSERTIONS=1 \
-s INITIAL_MEMORY=268435456 -s ALLOW_MEMORY_GROWTH=1 \
-s STACK_OVERFLOW_CHECK=2 -s PTHREAD_POOL_SIZE_STRICT=2 \
-o ${SOURCE}/final.html -I${SOURCE}/mylib/include -L${SOURCE}/mylib/lib64 \
-lOpenXLSX -lidbfs.js

echo "Finished Build"

製品をコンパイルし、

また、手動でボタンを追加し、ボタン イベントをバインドする必要もあります。

<button class="mybutton">运行我的函数</button>
<script>
            function downloadBlob(blob, filename) {
                const url = URL.createObjectURL(blob);

                const link = document.createElement('a');
                link.href = url;
                link.download = filename;

                document.body.appendChild(link);
                link.click();

                document.body.removeChild(link);
                URL.revokeObjectURL(url);
            }
            
            document.querySelector(".mybutton").addEventListener("click", function () {
                //alert("检查控制台");
                // Module._test_open_xlsx();
                Module.test_node_fs();
                var data = FS.readFile("/data/Spreadsheet.xlsx");
                var blob;
                blob = new Blob([data.buffer], { type: "application/vnd.ms-excel" });
                downloadBlob(blob, "Spreadsheet.xlsx");
            });
        </script>

最後に Web を公開して効果を確認します。

補足:jsはBlobバイナリオブジェクトをダウンロードします。

# CSDN `C知道`生成代码
# 补充:js 下载 Blob 二进制对象
要在JavaScript中下载一个Blob对象,你可以使用以下步骤:

1. 创建一个Blob对象,可以通过使用Blob构造函数或者从其他地方获取。
2. 创建一个URL对象,可以通过调用`URL.createObjectURL(blob)`来实现。这将为Blob对象创建一个临时URL。
3. 创建一个链接元素(`<a>`)并设置其`href`属性为临时URL。
4. 设置链接元素的`download`属性为所需的文件名。
5. 使用JavaScript模拟点击链接元素,以触发文件下载。

以下是一个示例代码:

```javascript
function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = filename;

  document.body.appendChild(link);
  link.click();

  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}
```

你可以调用`downloadBlob`函数并传入Blob对象和所需的文件名来下载Blob。例如:

```javascript
const data = 'Hello, world!';
const blob = new Blob([data], { type: 'text/plain' });

downloadBlob(blob, 'example.txt');
```

上述代码将下载一个名为`example.txt`的文本文件,内容为`Hello, world!`。

请注意,这段代码在浏览器环境下运行,不适用于Node.js。在Node.js中,你可以使用fs模块来完成文件下载操作。

7.3. NODEFS の使用例

// main_nodejs.cc
#include <OpenXLSX/OpenXLSX.hpp>
#include <emscripten/emscripten.h>
#include <emscripten/val.h>
#include <emscripten/bind.h> 

using namespace OpenXLSX;

void setup_nodefs() {
        EM_ASM(
                FS.mkdir('/data');
                FS.mount(NODEFS, {root:'.'}, '/data');
        );
}

int main() {
        setup_nodefs();
                 printf("call main function default\n");
        return 0;
}

void test_open_xlsx() {
    
    XLDocument doc;
    doc.create("/data/Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "EMSCRIPTEN_KEEPALIVE >>> Hello, OpenXLSX!";

    doc.save();

    printf("函数 test_open_xlsx 已被调用\n");

}


EMSCRIPTEN_BINDINGS(Module){
    emscripten::function("test_open_xlsx_v2",&test_open_xlsx);
}
#  build_nodejs.sh
export SOURCE=./wasm_demo

echo "Running Emscripten..."

emcc -std=c++17 --bind  ${SOURCE}/main_nodefs.cc \
-o ${SOURCE}/out.js -I${SOURCE}/mylib/include -L${SOURCE}/mylib/lib64 \
-lOpenXLSX -lnodefs.js

echo "Finished Build"

cd wasm_demo/

node out.js

ご覧のとおり、Excel ファイルが現在のディレクトリに生成されます。 

ここでのnodejs実行環境のバージョンは、emsdkのnodejsバージョンと一致している必要があることに注意してください。そうでない場合、プラットフォーム間で実行するときにバージョンの不一致によりエラーが報告されます。

7.4. 補足: Node WASI [インターフェイスが不安定であると公式に言われています]

# node 官网 https://nodejs.org/api/wasi.html
# 以下摘自 C知道
要在 Node.js 中使用 WASI,你需要安装 `wasi` 模块。以下是安装和使用的步骤:

1. 确保你已经安装了 Node.js,并且版本在 14 或更高。

2. 打开终端或命令提示符,并使用以下命令安装 `wasi` 模块:

   ```shell
   npm install wasi
   ```

3. 在你的 Node.js 项目中,通过以下方式引入 `wasi` 模块:

   ```javascript
   const { WASI } = require('wasi');
   ```

4. 创建一个新的 WASI 实例:

   ```javascript
   const wasi = new WASI({
     args: process.argv,
     env: process.env,
     preopens: { '/sandbox': '/path/to/sandbox' }
   });
   ```

   在上面的代码中,你可以通过 `args` 传递命令行参数,`env` 传递环境变量,`preopens` 指定预打开的目录。

5. 加载 WebAssembly 模块,并将其与 WASI 实例相关联:

   ```javascript
   const importObj = {
     wasi_snapshot_preview1: wasi.wasiImport
   };

   const wasmModule = new WebAssembly.Module(fs.readFileSync('/path/to/module.wasm'));
   const wasmInstance = new WebAssembly.Instance(wasmModule, importObj);
   ```

6. 启动 WASI 实例,并在其中运行 WebAssembly 模块:

   ```javascript
   wasi.start(wasmInstance);
   ```

这样,你就可以在 Node.js 中使用 WASI 运行 WebAssembly 模块了。请注意,WASI 目前仍然处于实验阶段,可能会有一些限制和不完善的地方。
# Step1、安装 wasi 模块
# npm install
cnpm install wasi

// node v18.x
// test_node_wasi.js 
'use strict';
const { readFile } = require('node:fs/promises');
const { WASI } = require('wasi');
const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
  args: argv,
  env,
  preopens: {
    '/sandbox': '/some/real/path/that/wasm/can/access',
  },
});

// Some WASI binaries require:
//   const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

(async () => {
  const wasm = await WebAssembly.compile(
    await readFile(join(__dirname, 'demo.wasm')),
  );
  const instance = await WebAssembly.instantiate(wasm, importObject);

  wasi.start(instance);
})();
node --experimental-wasi-unstable-preview1 test_node_wasi.js 
# 如果遇到报错
Cannot find module 'node:fs/promises'

# 这是 node 版本太低导致
[sam@localhost wasm_demo]$ node -v
v14.15.5

# 解决:升级 node 
清理 npm 缓存:npm cache clean -f
安装版本管理工具:npm install -g n
升级到最新的版本:n latest(最新版本)或者 n stable(最新稳定版本)

# 切换版本
n 18

# node 18 文档
https://nodejs.org/dist/latest-v18.x/docs/api/

8. OpenXLSXの基本操作のカプセル化 

1. C++ コードのカプセル化とコンパイル

// main_web_excel.cc
#include <OpenXLSX/OpenXLSX.hpp>
#include <emscripten/emscripten.h>
#include <emscripten/val.h>
#include <emscripten/bind.h> 
#include <string>
#include <iostream> 
#include <json11/json11.hpp>

using namespace json11;
using namespace std;
using namespace OpenXLSX;

// 手动挂载 IDBFS 文件系统
void setup_nodefs() {
	EM_ASM(
		FS.mkdir('/data');
		FS.mount(IDBFS, {root:'.'}, '/data');
	);
}

// 窄字符转宽字符
// 一开始是返回宽字符,发现Web输出乱码,又将结果以窄字符返回,显示正常
wstring string_to_wstring(string str){
    wstring wstr(str.length(), L' ');
    copy(str.begin(), str.end(), wstr.begin());
    return wstr;
}

// 将一个向量数组以分隔符拼接为字符串数组返回
string join(char c, vector<string> src) {
   
    string res = "";
    if (src.size() == 0) return res;

    vector<string>::iterator it = src.begin();
    res += "\"" + *it + "\"";

    for (it++; it != src.end(); it++) {
        res += c;
        res += "\"" + *it + "\"";
    }
    
    return res;
}

// 向量数组转字符串数组
string vec_to_array_str(vector<string> sheetNames) {
    string outputstr = "[";
    string sheetNamesStr = join(',',sheetNames);
    outputstr += sheetNamesStr;
    outputstr += "]";
    return outputstr;
}

// Excel 封装
class MyExcel {
    private:
        // 文件名
        string _filename;
        // 文件路径
        string _filePath;
        // 文档对象
        XLDocument _doc;
    public:
        // 构造函数,指定文件名,拼接虚拟路径
        MyExcel (string filename) {
            _filename = filename;
            _filePath = string("/data/") + filename;
        }
        // 创建文件
        void create(){
            _doc.create(_filePath);
        }
        // 打开文件
        void open(){
            _doc.open(_filePath);
            cout << "open file " << _filePath << endl;
        }
        // 关闭文件
        void close(){
            _doc.close();
            cout << "close file." << endl;
        }
        // 获取 Excel 全部的 sheet 
        vector<string> get_all_sheetname(){
            XLWorkbook wb = _doc.workbook();
            return wb.sheetNames();
        }
        // 加载某个 sheet 的全部内容
        string load_sheet_content(string sheetName){
            cout << "load_sheet_content " << sheetName <<endl;
            auto wks = _doc.workbook().worksheet(sheetName);
            cout << "rowCount: " << wks.rows().rowCount() << endl;
            string rowsJsonStr = "[";
            for (auto& row : wks.rows()) {
                vector<string> rowValue = vector<string> {};
                for (auto& value : list<XLCellValue>(row.values())) {
                    //rowValue.insert(rowValue.end(),(string) value);
                    if (value.typeAsString() == "float" ) {
                        rowValue.insert(rowValue.end(),to_string( value.get<float>() ));
                    } else if(value.typeAsString() == "integer" ) {
                        rowValue.insert(rowValue.end(),to_string( value.get<int>() ));
                    } else {
                        rowValue.insert(rowValue.end(),value.get<string>() );
                    }
                    cout << value.typeAsString() << endl;
                }

                //rowsJsonStr += "\"r" + to_string(row.rowNumber()) + "\"" + ":" + vec_to_array_str(rowValue);
                rowsJsonStr += vec_to_array_str(rowValue);
                if( row.rowNumber() != wks.rows().rowCount()) {
                    rowsJsonStr += ",";
                }
            }

            rowsJsonStr += "]";

            string out = "{";
            out += "\"rowsData\":" +  rowsJsonStr + ",";
            //out += "\"sheetName\":\"" + sheetName + "\",";
            out += "\"rowCount\":" + to_string(wks.rows().rowCount());
            out += "}";

            return out;           
        }
        // 将 json 字符解析并保存到 excel 
        string save_json_to_excel(string jsonstr) {
            string err;
            const auto json = Json::parse(jsonstr,err);
            cout << "Json::parse Err " << err << endl;
            const auto data = json["data"];
            const auto sheetList = json["sheetList"].array_items();
            for(int i = 0; i < sheetList.size(); i++) {
                const string sheetname = sheetList[i].string_value();
                const int rowCount = data[sheetname]["rowCount"].int_value();
                if (!_doc.workbook().sheetExists(sheetname)) {
                    _doc.workbook().addWorksheet(sheetname);
                }
                auto wks = _doc.workbook().worksheet(sheetname);
                //cout << sheetname << " " << rowCount << endl;
                for(int j = 0; j < rowCount; j++) {
                    // attention: j must begin from 1 to ... , since rowid > 0
                    vector<string> cellValues = vector<string> {};
                    const auto jsonArray = data[sheetname]["rowsData"][j].array_items();
                    for(int k = 0; k < jsonArray.size(); k++) {
                        cellValues.insert(cellValues.end(), jsonArray[k].string_value());
                    }
                    wks.row(j+1).values() = cellValues;
                }
            }
            cout << "Saving Excel File ..." << endl;
            _doc.save();
            return _filename;
        }
};

// 保存到 excel 文件,返回保存文件名
string save_excel(string str) {
    MyExcel myExcel = MyExcel("save.xlsx");
    myExcel.create();    
    string save_filename =  myExcel.save_json_to_excel(str);
    myExcel.close();
    return save_filename;
}

// 加载某个 excel 文档
string load_excel(string filename) {
    MyExcel myExcel = MyExcel(filename);
    myExcel.open();
    vector<string> sheetNames = myExcel.get_all_sheetname();
    cout << "sheet size: " << sheetNames.size() <<endl;
    string out = "{";
    out += "\"sheetList\":" + vec_to_array_str(sheetNames) + ",";
    out += "\"data\":{";
    for(int i = 0; i < sheetNames.size(); i++) {
        out += "\"" + sheetNames[i] + "\":" + myExcel.load_sheet_content(sheetNames[i]);
        if( i < sheetNames.size() - 1){
            out += ",";
        }
    }
    out += "}";
    out += "}";
    myExcel.close();    
    return out;
}

// 测试
void test_open_xlsx() {
    
    XLDocument doc;
    doc.create("/data/Spreadsheet.xlsx");
    auto wks = doc.workbook().worksheet("Sheet1");

    wks.cell("A1").value() = "EMSCRIPTEN_KEEPALIVE >>> Hello, OpenXLSX!";

    doc.save();

    printf("函数 test_open_xlsx 已被调用\n");

}

int main() {
        setup_nodefs();
        printf("call main function default\n");
        return 0;
}

EMSCRIPTEN_BINDINGS(Module){
    emscripten::function("test_open_xlsx_v2",&test_open_xlsx);
    emscripten::function("load_excel",&load_excel);
    emscripten::function("save_excel",&save_excel);
}

# build.sh
export SOURCE=./wasm_excel

echo "Running Emscripten..."

emcc -std=c++17 -Oz --bind ${SOURCE}/main_web_excel.cc ${SOURCE}/json11.cpp -s WASM=1 \
--shell-file ${SOURCE}/html_template/shell_minimal.html \
-s EXTRA_EXPORTED_RUNTIME_METHODS='[FS]' -s ASSERTIONS=1 \
-s INITIAL_MEMORY=268435456 -s ALLOW_MEMORY_GROWTH=1 \
-s STACK_OVERFLOW_CHECK=2 -s PTHREAD_POOL_SIZE_STRICT=2 \
-o ${SOURCE}/final.html -I${SOURCE}/mylib/include -L${SOURCE}/mylib/lib64 \
-lOpenXLSX -lidbfs.js

echo "Finished Build"

 2. フロントエンドコードのカプセル化

<!-- index.html -->
<!doctypehtml>
    <html lang=en-us>

    <head>
        <meta charset=utf-8>
        <meta content="text/html; charset=utf-8" http-equiv=Content-Type>
        <title>WASM + OpenXLSX</title>
        <link rel="stylesheet" href="excel.css" type="text/css">
        </link>
    </head>

    <body>
        <div class="top">
            <input type="file" id="file" onchange="loadExcel(event)">
            <button class="save_btn">SaveChange</button>
            <button class="download_btn">DownloadExcel</button>
        </div>

        <div class="wyb-excel wyb-excel-table">
            <table width="100%">
                <tbody>
                    <tr style="height: 38px;" id="letter">
                        <td class="drug-ele-td" style="width: 49px; text-align: center;">
                        </td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">A</td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">B</td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">C</td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">D</td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">E</td>
                        <td class="drug-ele-td" style="text-align: center; width: 91px;">F</td>
                        <td class="drug-ele-td" style="text-align: center; width: 92px;">G</td>
                        <td class="drug-ele-td" style="text-align: center; width: 92px;">H</td>
                        <td class="drug-ele-td" style="text-align: center; width: 93px;">I</td>
                        <td class="drug-ele-td" style="text-align: center; width: 93px;">J</td>
                    </tr>
                </tbody>
            </table>

        </div>
        <div class="sheetlist">
        </div>

        <script async src=final.js></script>
        <script src=index.js></script>
        <script type="text/javascript" src=jquery-3.4.1.js></script>
    </body>

    </html>
// index.js
let reader = new FileReader();
let jsonMap = Object.create({});
let globalSheetList = [];
let file;
let currentsheet;

// 保存并下载
document.querySelector(".download_btn").addEventListener("click", function () {
    // Module._test_open_xlsx();
    // Module.test_node_fs();
    const saveFilename = Module.save_excel(cast_excel_to_jsonstr());
    downloadExcel(saveFilename);
});

// 保存 sheet 修改
document.querySelector(".save_btn").addEventListener("click", function () {
    save_current_sheet_change();
});

/**
 * 下载 Excel 到本地
 * @param {*} filename 保存时的文件名
 */
function downloadExcel(filename) {
    if (filename) {
        var data = FS.readFile("/data/" + filename);
        var blob;
        blob = new Blob([data.buffer], { type: "application/vnd.ms-excel" });
        downloadBlob(blob, "Spreadsheet.xlsx");
    }
}

/**
 * 下载 blob 文件
 * @param {*} blob 二进制流数据
 * @param {*} filename 保存文件名
 */
function downloadBlob(blob, filename) {
    const url = URL.createObjectURL(blob);

    const link = document.createElement('a');
    link.href = url;
    link.download = filename;

    document.body.appendChild(link);
    link.click();

    document.body.removeChild(link);
    URL.revokeObjectURL(url);
}

/**
 * 将 excel 各个 sheet 的数据内容保存到本地对应的 json 对象中
 * @param {*} jsonObj json 对象
 */
function save_excel_json_to_local(jsonObj) {
    const sheetList = Object.keys(jsonObj.data);
    for (let i = 0; i < sheetList.length; i++) {
        jsonMap[sheetList[i]] = jsonObj.data[sheetList[i]];
    }
    globalSheetList = jsonObj["sheetList"];
}

/**
 * 保存当前 sheet 的修改
 */
function save_current_sheet_change() {
    jsonMap[currentsheet] = cast_current_sheet_to_jsonstr();
}

/**
 * 加载 Excel
 * @param {*} e 事件参数
 */
function loadExcel(e) {
    // 清空 jsonMap、globalSheetList
    jsonMap = {};
    globalSheetList = [];
    // 获取文件列表
    let files = document.getElementById('file').files;
    // 取第一个文件
    file = files[0];
    // 绑定加载事件
    reader.addEventListener('loadend', writeFile);
    // 读取文件为缓存数组
    reader.readAsArrayBuffer(file);
    // 定时读取文件内容输出到控制台
    setTimeout(() => {
        // 调用 c++ 函数 loadexcel,返回 json 字符串
        let jsonstr = Module.load_excel(file.name);
        // 清空旧数据
        $(".sheetlist").empty();
        // 序列化 json 字符
        let json = JSON.parse(jsonstr);
        save_excel_json_to_local(json);
        showTableList();
        // console.log(Object.keys(json.data)[0]);
        // 拿到 data 下的全部 sheet key,默认取第一个 sheet key 显示
        showCellList(Object.keys(json.data)[0]);
        // console.log('result: ' + jsonstr);
    }, 1000)
}

/**
 * 复制一份文件到容器路径下
 * @param {*} e 事件参数
 */
function writeFile(e) {
    let result = reader.result;
    const uint8_view = new Uint8Array(result);
    FS.writeFile('/data/' + file.name, uint8_view)
    console.log(uint8_view.byteLength)
}

/**
 * 渲染表格列表
 */
function showTableList() {
    let sheetElementStr = '';

    // 渲染 sheet 列表
    for (var i = 0; i < globalSheetList.length; i++) {
        if (i == 0) {
            sheetElementStr += `<div _na="${globalSheetList[i]}" class="currentsheet">${globalSheetList[i]}</div>`;
        } else {
            sheetElementStr += `<div _na="${globalSheetList[i]}">${globalSheetList[i]}</div>`;
        }
    }
    // append 元素
    $(".sheetlist").append(sheetElementStr);

    // 添加样式与点击事件
    $(".sheetlist>div").each(function () {
        $(this).click(function () {
            $(".sheetlist>div").each(function () {
                $(this).removeClass('currentsheet')
            })
            $(this).addClass('currentsheet');
            showCellList($(this).text())
        })
    })
}

/**
 * 渲染指定 sheet 的单元格数据
 * @param {*} sheetKey  sheet 名
 */
function showCellList(sheetKey) {
    currentsheet = sheetKey;

    let rowElementStr = '';
    // 拿到 sheet 的 rows 数据数组
    const currentSheetJson = jsonMap[sheetKey];
    const excelRowsData = currentSheetJson['rowsData'];
    const rowCount = currentSheetJson['rowCount'];
    // 第一层循环,渲染行数
    for (var j = 0; j < rowCount; j++) {
        rowElementStr += `<tr style="height: 38px;" >
        <td class="drug-ele-td" style="width: 48px; text-align: center;">${j + 1}</td>`;
        // 第二层循环,渲染列数,这里不确定有多少列,默认了 10 列
        for (var i = 0; i < 10; i++) {
            if (excelRowsData[j][i]) {
                rowElementStr += `<td style="width: 90px;">${excelRowsData[j][i]}</td>`;
            } else {
                rowElementStr += `<td style="width: 90px;"></td>`;
            }
        }
        rowElementStr += `</tr>`;
    }
    // 移除旧数据元素
    $("table tbody tr").not("#letter").remove()
    // 渲染新数据元素
    $("table tbody").append(rowElementStr);
    // 新增单元格的点击事件
    $("td").not('.drug-ele-td').each(function () {
        $(this).click(function () {
            // 新增属性,内容可编辑
            $(this).attr('contenteditable', "true");
            // 新增可编辑时的样式
            $(this).addClass('contenteditable', "true")
        })
    })
}

/**
 * Excel sheet 内容转 json
 */
function cast_current_sheet_to_jsonstr() {

    const obj = Object.create({});

    // 获取当前 sheet 全部行
    let rowTrList = $("table tbody tr").not("#letter");
    let rowCount = rowTrList.length;
    let totalRow = [];
    for (var i = 0; i < rowTrList.length; i++) {
        // 获取该行元素
        let colsList = $(rowTrList[i]).children();
        // 缓存该行每列的数据
        let rowData = [];
        for (var j = 1; j < colsList.length; j++) {
            let td = $(colsList[j]);
            var textValue = td.text();
            rowData.push(textValue);
        }
        totalRow.push(rowData);
    }

    obj.rowCount = rowCount;
    obj.rowsData = totalRow;

    return obj;
}

/**
 * 把 Excel 转 json
 */
function cast_excel_to_jsonstr() {
    const obj = Object.create({});
    obj.data = jsonMap;
    obj.sheetList = globalSheetList;
    // console.log(JSON.stringify(obj));
    return JSON.stringify(obj);
}

3. 運用効果 

npm start

Excel の読み込み、Excel を修正して保存、Excel をダウンロードする機能はすべて正常であることがわかります。

4. 踏みつけ記録 

4.1. wstring と string について

当初は中国語との互換性を考慮して wstring を返していましたが、Web 出力が文字化けしていることが判明し、最終的には一律に string を返すようになり、文字化けの問題は解決されました。

 文字列を返す、

4.2. 大きな数値と日付型の分析について

大きな数値と日付は整数に変換され、オーバーフローした大きな数値は 1 に変換され、日付と時刻は浮動小数点数に変換されます。

これら両方の問題は、すべて文字列形式のセル単位形式を使用しない限り、解決策はありません。

4.3. シートの命名形式と行数について

 シート名と行数におかしなバグがあり、シート名にアンダースコア_が含まれている場合、正常に1k行程度のデータは読み取れるのですが、1w行程度のデータの読み込みに失敗します。削除すると、1w 行を正常に読み取ることができます。

もう 1 つ注意すべき点は、データを書き込むときは行番号を 1 から始める必要があり、そうしないとエラーが報告されます。

4.4. 補足: json11を使用する 

最初はどのシリアル化ツールが使いやすいのかわからず、いろいろ遠回りしましたが、最終的には json11 を使うことにしました。

使用 json11 库,官网 https://gitcode.net/mirrors/dropbox/json11?utm_source=csdn_github_accelerator,

# 克隆代码
git clone https://gitcode.net/mirrors/dropbox/json11.git

この例を使用して、2 つの json11 ファイルをプロジェクトのルート ディレクトリにコピーします。

プロジェクトに追加、

テストコード、参照ヘッダーファイル、名前空間、

#include <iostream> 
#include "json11.hpp"

using namespace json11;
using namespace std;

void test_json() {
    Json my_json = Json::object{
            { "key1", "value1" },
            { "key2", false },
            { "key3", Json::array { 1, 2, 3 } },
    };
    std::string json_str = my_json.dump();
    cout << json_str << endl;
}

int main() {
    test_json();
    return 0;
}

4.5. 補足: 整形ツール fmt

# 参考
https://zhuanlan.zhihu.com/p/590654984?utm_id=0
https://github.com/fmtlib/fmt
https://fmt.dev/latest/index.html
https://hackingcpp.com/cpp/libs/fmt.html
# fmt 下载地址
https://github.com/fmtlib/fmt/releases/download/10.0.0/fmt-10.0.0.zip
https://github.com/fmtlib/fmt.git

5. 欠点とTODO 

私は C++ と Web の初心者であり、関数コードはすべて単純な原則に基づいて実装されているため、多くの欠点や改善が必要な領域があります。

5.1. C++ でカプセル化された各関数は引き続き最適化できます。

5.2. json11 ライブラリを使用して json オブジェクトの戻り文字を最適化する

5.3. コンパイルスクリプトの最適化

5.4. C++ でファイルを保存する場合、uuid を使用してファイル名を生成します。

5.5. 新しい Web 関数には、行の追加、行の削除、および単純な関数が含まれます。

6. 補足: uuid を使用する

# 官网 https://www.boost.org/
# 下载后解压
tar -zxvf boost_1_82_0.tar.gz

 使用例、

// main.cc
#include <iostream>
#include <string>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid_generators.hpp>

using namespace std;

string Genuuid()
{
    boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
    return boost::uuids::to_string(a_uuid);
}

int main(){
    cout << Genuuid() << endl;
}
# build.sh
g++ -std=c++17 main.cc -I /home/sam/Downloads/boost_1_82_0/ -o gen_uuid
./gen_uuid

wasm-excel プロジェクトに統合され、

// main_web_excel.cc

string gen_uuid() {
    boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
    return boost::uuids::to_string(a_uuid);
}

string save_excel(string str) {
    MyExcel myExcel = MyExcel(gen_uuid() + ".xlsx");
    myExcel.create();    
    string save_filename =  myExcel.save_json_to_excel(str);
    myExcel.close();
    return save_filename;
}
# build.sh
export SOURCE=./wasm_excel

echo "Running Emscripten..."

emcc -std=c++17 -Oz --bind ${SOURCE}/main_nodefs.cc ${SOURCE}/json11.cpp -s WASM=1 \
--shell-file ${SOURCE}/html_template/shell_minimal.html \
-s EXTRA_EXPORTED_RUNTIME_METHODS='[FS]' -s ASSERTIONS=1 \
-s INITIAL_MEMORY=268435456 -s ALLOW_MEMORY_GROWTH=1 \
-s STACK_OVERFLOW_CHECK=2 -s PTHREAD_POOL_SIZE_STRICT=2 \
-o ${SOURCE}/final.html -I${SOURCE}/mylib/include -L${SOURCE}/mylib/lib64 \
-I /home/sam/Downloads/boost_1_82_0/ \
-lOpenXLSX -lidbfs.js

echo "Finished Build"

テスト効果、

ご覧のとおり、ファイル名には uuid が使用されています。 

9. 参考資料

1. C/C++ を WebAssembly にコンパイルする - WebAssembly | MDN

2、メイン — Emscripten 3.1.44-git (dev) ドキュメント

3. C++ダイナミックリンクライブラリ(DLL)の作成と呼び出し - Zhihu

4、GitHub - trollal/OpenXLSX: Microsoft Excel® (.xlsx) ファイルの読み取り、書き込み、作成、変更のための C++ ライブラリ。

5. Python インストール エラーの解決策:「ModuleNotFoundError: No modulenamed _ctypes」_Six Finger Heixiai のブログ-CSDN ブログ

6. Linux の gcc を使用した静的ライブラリと動的ライブラリの作成と使用_gcc は静的ライブラリを指定します_月のブログ - CSDN ブログ

7、したいのですが… - WebAssembly

8、WebAssembly システム インターフェイス (WASI) | Node.js v20.5.0 ドキュメント

9、ワシ - 海抜

10. 【C++】WebAssemblyを使ってExcel_wasmファイルをブラウザ側で操作する 開き方_Your Menthol’s Blog - CSDN Blog

11. webassembly Web ページのプラグイン不要の再生テクノロジ - Zhihu

12. Rust Wasm Linux開発環境の構築_centosへのrustのインストール_Yu Shanmaのブログ - CSDNブログ

13、ミラー/ドロップボックス/json11・GitCode

14. Emscripten について | Emscripten

15、GitHub - fmtlib/fmt: 最新の書式設定ライブラリ

16、C++ ライブラリを強化する

おすすめ

転載: blog.csdn.net/weixin_47560078/article/details/131986572