Skip to content

Commit a769cf5

Browse files
Merge pull request #140 from paulocoutinhox/support-wasm-standalone
add support for wasm standalone
2 parents f62e989 + 3be753b commit a769cf5

File tree

7 files changed

+196
-23
lines changed

7 files changed

+196
-23
lines changed

.github/workflows/android.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- name: Install Ninja
4444
uses: seanmiddleditch/gha-setup-ninja@master
4545
with:
46-
version: "1.10.0"
46+
version: "1.12.1"
4747

4848
- name: Install NDK
4949
uses: nttld/setup-ndk@v1

.github/workflows/ios.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
config:
18-
- { name: "macOS", os: "macos-latest", target: "ios" }
18+
- { name: "macOS", os: "macos-15", target: "ios" }
1919

2020
steps:
2121
- uses: actions/checkout@v4
2222

23+
- name: Set up Xcode
24+
run: sudo xcode-select -s "/Applications/Xcode_26.0.app"
25+
2326
- name: Set up Python
2427
uses: actions/setup-python@v5
2528
with:
@@ -36,7 +39,7 @@ jobs:
3639
- name: Install Ninja
3740
uses: seanmiddleditch/gha-setup-ninja@master
3841
with:
39-
version: "1.10.0"
42+
version: "1.12.1"
4043

4144
- name: Verify
4245
run: |

.github/workflows/macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
- name: Install Ninja
3737
uses: seanmiddleditch/gha-setup-ninja@master
3838
with:
39-
version: "1.10.0"
39+
version: "1.12.1"
4040

4141
- name: Verify
4242
run: |

.github/workflows/wasm.yml

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
- name: Install Ninja
4242
uses: seanmiddleditch/gha-setup-ninja@master
4343
with:
44-
version: "1.10.0"
44+
version: "1.12.1"
4545

4646
- name: Install EMSDK
4747
run: |
@@ -98,6 +98,9 @@ jobs:
9898
- name: Generate
9999
run: python3 make.py generate-${{ matrix.config.target }}
100100

101+
- name: Test WasmTime
102+
run: python3 make.py test-${{ matrix.config.target }}time
103+
101104
- name: Archive
102105
run: python3 make.py archive-${{ matrix.config.target }}
103106

@@ -107,12 +110,6 @@ jobs:
107110
name: artifact-${{ matrix.config.target }}
108111
path: ${{ matrix.config.target }}.tgz
109112

110-
- name: Save per-config package (release)
111-
uses: actions/upload-artifact@v4
112-
with:
113-
name: artifact-${{ matrix.config.target }}-release
114-
path: ${{ matrix.config.target }}-release.tgz
115-
116113
deploy:
117114
name: Deploy
118115
runs-on: ubuntu-latest
@@ -142,12 +139,3 @@ jobs:
142139
asset_path: wasm.tgz
143140
asset_name: wasm.tgz
144141
asset_content_type: application/tar+gzip
145-
- name: Upload per-config package (release)
146-
uses: actions/upload-release-asset@v1
147-
env:
148-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
149-
with:
150-
upload_url: ${{ steps.get_release.outputs.upload_url }}
151-
asset_path: wasm-release.tgz
152-
asset_name: wasm-release.tgz
153-
asset_content_type: application/tar+gzip

make.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
- build-wasm
5050
- install-wasm
5151
- test-wasm
52+
- test-wasmtime
5253
- generate-wasm
5354
- publish-wasm
5455
- publish-to-web-wasm
@@ -211,6 +212,10 @@ def main(options):
211212
elif task == "test-wasm":
212213
wasm.run_task_test()
213214

215+
# test - wasmtime
216+
elif task == "test-wasmtime":
217+
wasm.run_task_test_wasmtime()
218+
214219
# generate - wasm
215220
elif task == "generate-wasm":
216221
wasm.run_task_generate()

modules/wasm.py

Lines changed: 179 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,164 @@ def run_task_test():
481481
l.ok()
482482

483483

484+
# -----------------------------------------------------------------------------
485+
def run_task_test_wasmtime():
486+
from pathlib import Path
487+
488+
from wasmtime import Engine, FuncType, Linker, Module, Store, WasiConfig
489+
490+
l.colored("Testing with wasmtime...", l.YELLOW)
491+
492+
current_dir = f.current_dir()
493+
494+
for config in c.configurations_wasm:
495+
for target in c.targets_wasm:
496+
l.colored(
497+
'Testing arch "{0}" and configuration "{1}"...'.format(
498+
target["target_cpu"], config
499+
),
500+
l.YELLOW,
501+
)
502+
503+
# paths
504+
relative_dir = os.path.join(
505+
"build",
506+
target["target_os"],
507+
target["target_cpu"],
508+
config,
509+
)
510+
511+
root_dir = os.path.join(current_dir, relative_dir)
512+
node_dir = os.path.join(root_dir, "node")
513+
wasm_file = os.path.join(node_dir, "pdfium.std.wasm")
514+
515+
# check if wasm file exists
516+
if not f.file_exists(wasm_file):
517+
l.e(f"WASM file not found: {wasm_file}")
518+
continue
519+
520+
l.bullet(f"WASM file: {wasm_file}", l.YELLOW)
521+
522+
# create engine and load the pdfium.wasm module
523+
engine = Engine()
524+
module = Module.from_file(engine, wasm_file)
525+
526+
# create WASI context and store
527+
wasi_config = WasiConfig()
528+
wasi_config.inherit_stdin()
529+
wasi_config.inherit_stdout()
530+
wasi_config.inherit_stderr()
531+
532+
store = Store(engine)
533+
store.set_wasi(wasi_config)
534+
535+
# create a linker and add WASI support
536+
linker = Linker(engine)
537+
linker.define_wasi()
538+
539+
# define stub functions for unknown imports
540+
for imp in module.imports:
541+
module_name = imp.module
542+
field_name = imp.name
543+
544+
# check if this is a function import
545+
if isinstance(imp.type, FuncType):
546+
func_type = imp.type
547+
548+
# create a stub function that returns default values
549+
def make_stub(ft):
550+
def stub_func(*_args):
551+
# return default values (0 or None) based on results
552+
if ft.results:
553+
if len(ft.results) == 1:
554+
return 0
555+
return tuple(0 for _ in ft.results)
556+
return None
557+
558+
return stub_func
559+
560+
try:
561+
linker.define_func(
562+
module_name, field_name, func_type, make_stub(func_type)
563+
)
564+
except Exception:
565+
# already defined (e.g., by WASI)
566+
pass
567+
568+
# instantiate the module
569+
instance = linker.instantiate(store, module)
570+
exports = instance.exports(store)
571+
572+
# get and call FPDF_InitLibrary function
573+
try:
574+
init_library = exports["FPDF_InitLibrary"]
575+
init_library(store)
576+
l.bullet("FPDF_InitLibrary successfully called", l.GREEN)
577+
except KeyError:
578+
l.e("Function 'FPDF_InitLibrary' not found")
579+
continue
580+
581+
# test with a sample PDF file
582+
sample_pdf = os.path.join(
583+
current_dir, "sample-wasm", "assets", "web-assembly.pdf"
584+
)
585+
586+
if not f.file_exists(sample_pdf):
587+
l.bullet("Sample PDF not found, skipping document test", l.PURPLE)
588+
continue
589+
590+
l.bullet(f"Testing with PDF: {sample_pdf}", l.YELLOW)
591+
592+
# read PDF data
593+
pdf_path = Path(sample_pdf)
594+
pdf_data = pdf_path.read_bytes()
595+
596+
# allocate memory for the PDF data
597+
malloc = exports["malloc"]
598+
memory = exports["memory"]
599+
600+
# allocate buffer in WASM memory
601+
buf_ptr = malloc(store, len(pdf_data))
602+
mem_data = memory.data_ptr(store)
603+
604+
# copy PDF data to WASM memory
605+
for i, byte in enumerate(pdf_data):
606+
mem_data[buf_ptr + i] = byte
607+
608+
# load the PDF document from memory
609+
fpdf_load_mem_document = exports["FPDF_LoadMemDocument"]
610+
doc = fpdf_load_mem_document(store, buf_ptr, len(pdf_data), 0)
611+
612+
if doc == 0:
613+
get_last_error = exports["FPDF_GetLastError"]
614+
error = get_last_error(store)
615+
l.e(f"Failed to load PDF. Error code: {error}")
616+
617+
# free the allocated memory
618+
free = exports["free"]
619+
free(store, buf_ptr)
620+
continue
621+
622+
# get page count
623+
fpdf_get_page_count = exports["FPDF_GetPageCount"]
624+
page_count = fpdf_get_page_count(store, doc)
625+
626+
l.bullet(f"PDF: {pdf_path.name}", l.GREEN)
627+
l.bullet(f"Number of pages: {page_count}", l.GREEN)
628+
629+
# close the document
630+
fpdf_close_document = exports["FPDF_CloseDocument"]
631+
fpdf_close_document(store, doc)
632+
633+
# free the allocated memory
634+
free = exports["free"]
635+
free(store, buf_ptr)
636+
637+
l.bullet("Test completed successfully", l.GREEN)
638+
639+
l.ok()
640+
641+
484642
# -----------------------------------------------------------------------------
485643
def run_task_generate():
486644
l.colored("Generating...", l.YELLOW)
@@ -591,7 +749,8 @@ def run_task_generate():
591749
"{0}".format("-g" if config == "debug" else "-O2"),
592750
"-s",
593751
f"EXPORTED_FUNCTIONS={complete_functions_list}",
594-
"-s", "ALLOW_TABLE_GROWTH",
752+
"-s",
753+
"ALLOW_TABLE_GROWTH",
595754
"-s",
596755
'EXPORTED_RUNTIME_METHODS=\'["ccall", "cwrap", "wasmExports", "HEAP8", "HEAP16", "HEAP32", "HEAPU8", "HEAPU16", "HEAPU32", "HEAPF32", "HEAPF64", "addFunction", "removeFunction", "setValue"]\'',
597756
"custom.cpp",
@@ -624,12 +783,27 @@ def run_task_generate():
624783
l.colored("Compiling ES6 module with emscripten...", l.YELLOW)
625784
es6_command = [
626785
*base_command,
627-
"-s" "EXPORT_ES6=1",
786+
"-s",
787+
"EXPORT_ES6=1",
628788
"-o",
629789
os.path.join(gen_out_dir, "pdfium.esm.js"),
630790
]
631791
r.run(" ".join(es6_command), cwd=gen_utils_dir, shell=True)
632792

793+
# Generate STANDALONE module, only .js will be generated (no .wasm)
794+
l.colored("Compiling STANDALONE module with emscripten...", l.YELLOW)
795+
std_command = [
796+
*base_command,
797+
"-s" "STANDALONE_WASM=1",
798+
"-sSTANDALONE_WASM=1",
799+
"-sWASM_ASYNC_COMPILATION=0",
800+
"-sWARN_ON_UNDEFINED_SYMBOLS=1",
801+
"-sERROR_ON_UNDEFINED_SYMBOLS=0",
802+
"-o",
803+
os.path.join(gen_out_dir, "pdfium.std.js"),
804+
]
805+
r.run(" ".join(std_command), cwd=gen_utils_dir, shell=True)
806+
633807
# copy files
634808
l.colored("Copying compiled files...", l.YELLOW)
635809

@@ -769,7 +943,9 @@ def run_task_archive():
769943
)
770944

771945
# Create per config "npm install"-compatible tarball
772-
per_config_tar = tarfile.open(os.path.join(current_dir, f"wasm-{config}.tgz"), "w:gz")
946+
per_config_tar = tarfile.open(
947+
os.path.join(current_dir, f"wasm-{config}.tgz"), "w:gz"
948+
)
773949
per_config_tar.add(
774950
name=lib_dir,
775951
# Use "package" as the root directory to be compatible with "npm install"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
docopt
22
black
33
pygemstones>=0.0.13
4+
wasmtime

0 commit comments

Comments
 (0)