ledger icon indicating copy to clipboard operation
ledger copied to clipboard

Ledger-cli in WebAssembly

Open alensiljak opened this issue 4 years ago • 16 comments

Perhaps a crazy idea but an interesting one, nonetheless. Is it possible to compile ledger-cli into wasm? Has anyone tried, what are the challenges? Opening this issue as a placeholder for the topic.

The benefits should be obvious, I guess. Having Ledger compiled to Wasm would allow running it natively in browsers, including PWAs, and also on Android. I've seen some people compiling Boost with Emscripten but I'm quite new to the topic at the moment.

~~Presumably, file reading would be one of the main issues~~

alensiljak avatar Dec 31 '21 09:12 alensiljak

Seems there is also a virtual filesystem available, which is convenient for storing and reading the files.

  • https://stackoverflow.com/questions/47313403/passing-client-files-to-webassembly-from-the-front-end
  • https://kripken.github.io/emscripten-site/docs/porting/files/file_systems_overview.html

Files can be stored separately and preloaded into the virtual file system.

  • http://www.jamesfmackenzie.com/2019/12/08/webassembly-loading-files/

alensiljak avatar Dec 31 '21 12:12 alensiljak

Interesting idea! It would permit me to run Ledger directly on the Internet Computer too.

jwiegley avatar Jan 09 '22 22:01 jwiegley

Filesystem extension https://github.com/jvilk/BrowserFS

alensiljak avatar Jan 21 '22 07:01 alensiljak

Link to WebAssembly support issue in Boost repo.

alensiljak avatar Aug 26 '22 15:08 alensiljak

There is already #2004 but nowadays it seems that a WASI target would be a perfect fit for Ledger-CLI.

In addition, OPFS is now available is all major browsers, effectively providing the conditions for Ledger to run in a browser, using wasm target.

alensiljak avatar May 13 '23 20:05 alensiljak

There is also Chirp - https://github.com/leaningtech/cheerp-meta - compiler.

alensiljak avatar May 16 '23 09:05 alensiljak

I just managed to get ledger compiled with emscripten following the steps stated in https://github.com/ledger/ledger/issues/2004#issuecomment-1878164915 with changes in https://github.com/gudzpoz/ledger/commit/3d131345ae86616b92c0e35013698e14a0089114 applied.

By the way, currently it seems to me that emscripten is the de facto compiler to port C++ code to WASM:

If we are to target JS platforms, should a JS/TypeScript wrapper be supplied (for, e.g., libledger)? (Or maybe it is to be put in a separate repository?) Working with WASM seems to involve all kinds of manual memory management and can be painful when one has no prior experiences. (I wrote a simple ledger-cli wrapper to test things out here: https://gist.github.com/gudzpoz/009fc2445ab717d2e049eb069040f9b4 )

gudzpoz avatar Jan 05 '24 07:01 gudzpoz

That's great news, @gudzpoz, congratulations! Thanks for putting in the time and effort. I'll have a closer look at (your comment in #2004](https://github.com/ledger/ledger/issues/2004#issuecomment-1878164915) and will think about how this could be integrated into ledger's current CMake-based build process. In your mind what are good next steps to move forward with this?

afh avatar Jan 05 '24 09:01 afh

I am not exactly sure though... Maybe a typical goal is to write a proper JS/TS wrapper and have ledger published in NPM so that ledger-cli can be readily installed with npm install -g ledger....

Also, maybe we should discuss ways to expose the libledger API as WASM functions? Personally I am satisfied with WASM ledger-cli, and implementing & maintaining & testing the WASM-libledger API would require a lot of work. But, anyway, it can be done theoretically :-P

And concerning testing, with a bit of tweaking (https://github.com/gudzpoz/ledger/commit/0ef943b6b813179b56548c99df745fc74da7896e) we can now run the tests against the compiled bundle and at least some of them are passing: 85% tests passed, 63 tests failed out of 421. I haven't looked into the failed ones yet, but many of them are likely to be due to my poorly written wrapper.

Edit: Now 97% tests passed, 13 tests failed out of 421:

The following tests FAILED:
	  1 - DocTestsTest_ledger3 (Failed) <-- No error when manually run?
	 45 - BaselineTest_dir-import_py (Failed)
	 47 - BaselineTest_dir-python_py (Failed)
	 57 - BaselineTest_feat-import_py (Failed)
	 58 - BaselineTest_feat-option_py (Failed)
	 60 - BaselineTest_feat-value_py (Failed)
	 91 - BaselineTest_opt-color (Failed)
	309 - RegressTest_4D9288AE_py (Failed)
	347 - RegressTest_78AB4B87_py (Failed)
	360 - RegressTest_9188F587_py (Failed)
	377 - RegressTest_B21BF389_py (Failed)
	381 - RegressTest_BF3C1F82 (Failed) 
	421 - RegressTest_xact_code_py (Failed)

Most of them are about python, which is not supported anyway. BF3C1F82 may be due to a bug (or undefined behavior?) of strptime in emscripten: it converts "2012/02/30" into tm(tm_year=2012, tm_mon=2, tm_mday=1).

gudzpoz avatar Jan 05 '24 14:01 gudzpoz

A bit OT but I'd like to try the Wasm version from a Blazor app. Apparently it can invoke Wasm methods directly so no js wrapper needed. It would be cool if this would become an official build. That would give ledger a much wider reach. Is emscripten using OPFS or it's own storage mechanism? Where is ledger wasm expecting to find files? Where can we have discussion on related topics? I wouldn't want to spam this thread outside the actual Wasm version. Once that is ticked off, I think I'd try Wasm and Wasi versions. Wasi would be a particularly interesting build for Windows users as there's no official version, as far as I can tell. Great job and thank you for the effort. I find this is a significant breakthrough. 2024 is expected to bring Wasm to the forefront, or so they say.

alensiljak avatar Jan 05 '24 20:01 alensiljak

The problems is that the Wasm build itself will most likely require a js wrapper due to usage of C++ exceptions in ledger.

As is summarized in https://github.com/crystal-lang/crystal/issues/13130:

  • Native exception handling in Wasm is still an ongoing proposal and not all Wasm runtimes support it. (Emscripten supports using Wasm native exception handling by setting -fwasm-exceptions.)
  • When compiled with -fexceptions instead in emscripten, "JS-based exceptions works by calling into JavaScript and having a try-catch block there, that in turn calls into Wasm. Throwing an exception works by invoking JavaScript again to throw."

Taking the first approach (with flags like -fwasm-exceptions and -sSTANDALONE_WASM=1), it should be possible to use without a wrapper, and the Wasm bundle should hopefully comply with WASI standards - what FS it uses will depend on the runtime you use. But, again, runtimes supporting Wasm native exceptions are scarce and most of them are ... browsers, which don't seem to provide a WASI interface anyway.

With the current -fexceptions approach, since it requires a wrapper, then it needs JS after all. (The filesystems used by the JS wrapper is probably implemented by emscripten and provide their own API.)

gudzpoz avatar Jan 06 '24 09:01 gudzpoz

Concerning python bindings, it seems that pyodide (Wasm port of CPython) has in-tree support for boost, libgmp and libmpfr, and it should be possible to build ledger using pyodide-build while supporting python bindings. But I have no idea how this can get integrated into CMake building process though...

gudzpoz avatar Jan 09 '24 10:01 gudzpoz

My head hurts just trying to imagine what the output of that should be. How should Python integration work with respect to the Wasm constraints?

alensiljak avatar Jan 09 '24 15:01 alensiljak

Could we summarize what the available options are, the level of difficulties and the steps required to get them to fruition, as well as some common use cases which this would be intended for?

For example, I wouldn't mind having support in browser only, for start. I'd like to try this and see how to pass the files from OPFS. This in itself would be a fantastic milestone.

BTW, maybe Wasm modules will make the python integration obsolete?

Edit: The primary use case would then be integration into a PWA, and providing a GUI in a browser which uses ledger directly instead of a Web server executable calling it in a shell.

alensiljak avatar Jan 29 '24 18:01 alensiljak

image

Just built a ledger python wheel that runs in browsers following the steps to build an in-tree Pyodide package.

One minor change in packages/boost-cpp/meta.yaml was necessary to let pyodide produce some boost libraries:

diff --git a/packages/boost-cpp/meta.yaml b/packages/boost-cpp/meta.yaml
index 9106dd4a..d9c86e53 100644
--- a/packages/boost-cpp/meta.yaml
+++ b/packages/boost-cpp/meta.yaml
@@ -19,7 +19,7 @@ build:
     printf "using clang : emscripten : emcc : <archiver>emar <ranlib>emranlib <linker>emlink ;" | tee -a ./project-config.jam
 
     ./b2 variant=release toolset=clang-emscripten link=static threading=single \
-      --with-date_time --with-filesystem \
+      --with-date_time --with-filesystem --with-iostreams --with-nowide --with-python \
       --with-system --with-regex --with-chrono --with-random --with-program_options --disable-icu \
       cxxflags="$SIDE_MODULE_CXXFLAGS -fexceptions -DBOOST_SP_DISABLE_THREADS=1" \
       cflags="$SIDE_MODULE_CFLAGS -fexceptions -DBOOST_SP_DISABLE_THREADS=1" \

The following was the contents of packages/ledger/meta.yaml:

package:
  name: ledger
  version: "3.3.2"
  top-level:
    - ledger

source:
  path: https://github.com/gudzpoz/ledger/archive/refs/heads/master.zip
  sha256: 0659cfb2812d14a72a6388c1f8acfcd0e39f279713e4e0a30937d3821682edce

requirements:
  host:
    - boost-cpp
    - libgmp
    - libmpfr

build:
  script: |
    export INSTALL_DIR=${WASM_LIBRARY_DIR}
    sed -i.bak -E "s/COMPONENTS Interpreter Development/COMPONENTS Interpreter/" CMakeLists.txt
    sed -i.bak -E "s/HAVE_GETPWUID|HAVE_GETPWNAM/0/" src/utils.cc
    mkdir -p build
    cd build
    emcmake cmake -DBUILD_LIBRARY=ON -DUSE_PYTHON=ON \
      -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR} \
      -DEMSCRIPTEN=ON -DHAVE_BOOST_PYTHON=ON \
      -DPython_INCLUDE_DIRS=$PYTHONINCLUDE \
      -DBoost_ROOT=$WASM_LIBRARY_DIR \
      -DBoost_INCLUDE_DIR=$WASM_LIBRARY_DIR/include \
      -DBOOST_LIBRARYDIR=$WASM_LIBRARY_DIR/lib \
      -DBoost_DATE_TIME_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_date_time.a \
      -DBoost_FILESYSTEM_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_filesystem.a \
      -DBoost_IOSTREAMS_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_iostreams.a \
      -DBoost_NOWIDE_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_nowide.a \
      -DBoost_PYTHON_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_python$PYMAJOR$PYMINOR.a \
      -DBoost_REGEX_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_regex.a \
      -DBoost_SYSTEM_LIBRARY_RELEASE=$WASM_LIBRARY_DIR/lib/libboost_system.a \
      -DGMP_PATH=$WASM_LIBRARY_DIR/include \
      -DGMP_LIB=$WASM_LIBRARY_DIR/lib/libgmp.a \
      -DMPFR_PATH=$WASM_LIBRARY_DIR/include \
      -DMPFR_LIB=$WASM_LIBRARY_DIR/lib/libmpfr.a \
      -DUTFCPP_INCLUDE_DIR=$PWD/../lib/utfcpp/v4/source \
      ..
    emmake make -j ${PYODIDE_JOBS:-3} libledger
    cp ledger.so $DISTDIR
    echo "from setuptools import setup" > ../setup.py
    echo "setup(name='ledger', version='3.3.2', packages=[''], package_dir={'': '$DISTDIR'}," >> ../setup.py
    echo "      package_data={'': ['ledger.so']})" >> ../setup.py

about:
  home: https://ledger-cli.org
  summary: Double-entry accounting system with a command-line reporting interface
  license: LGPL-3.0+

And, no, I don't think Wasm modules will make the python integration obsolete.


Concerning possible options, the first few questions that occur to me are:

  1. Does the Ledger project plan to publish official builds for WASM builds / WASM-built python wheel?

    Currently most ledger binaries are mostly maintained by package maintainers from different distros. However, ecosystems around JS/Python somehow promote a more centralized way of package distribution: developers are often expected to build & publish "official" binaries/bundles to a centralized store (NPM/PyPI). Doing this of course puts burdens on the project, like secret key management and maintenance of the JS/TS wrapper.

  2. Is it ever possible to modify acprep to set up environment for WASM builds? (Or is there a need for it in PRs?)

    It seems to me the acprep prepares for local builds but WASM builds sound more like cross-compiling (requiring custom-built boost libgmp libmpfr...). Using pyodide simplifies the job but I have no idea if it works across platforms. Is this part expected in a PR? I am rather convinced that the pyodide approach can build in GitHub workflows but providing a cross-platform set-up script seems a bit overwhelming to me.

gudzpoz avatar Feb 01 '24 10:02 gudzpoz