diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 79ff9cb4..00000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -* -!package.json -!package-lock.json -!tsconfig.json -!tsconfig.base.json -!bin -!src diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 90a53373..e534849d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,6 +52,12 @@ jobs: run: | make build/libllhttp.a + - name: Upload build + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: build-${{ runner.os }} + path: build/ + test: name: Run tests runs-on: ${{ matrix.os }} @@ -116,3 +122,33 @@ jobs: - name: Run lint command run: npm run lint + + build-wasm: + name: Build WebAssembly + runs-on: ubuntu-latest + steps: + - name: Fetch code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 1 + + - name: Restore node_modules cache for Linux + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + if: runner.os == 'Linux' + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Build + run: npm run build-wasm + + - name: Upload build + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: build-wasm + path: build/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d7e90922..00000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM node:22-alpine@sha256:ed9736a13b88ba55cbc08c75c9edac8ae7f72840482e40324670b299336680c1 -ARG UID=1000 -ARG GID=1000 - -RUN apk add -U clang lld wasi-sdk && mkdir /home/node/llhttp - -WORKDIR /home/node/llhttp - -COPY . . - -RUN npm ci - -USER node diff --git a/bin/build_wasm.ts b/bin/build_wasm.ts index 40d0efb8..94c3e38d 100644 --- a/bin/build_wasm.ts +++ b/bin/build_wasm.ts @@ -1,97 +1,119 @@ -import { execSync } from 'child_process'; -import { copyFileSync, mkdirSync } from 'fs'; -import { join, resolve } from 'path'; +import { execSync } from 'node:child_process' +import { writeFileSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs' +import { join, resolve } from 'node:path' -let platform = process.env.WASM_PLATFORM ?? ''; -const WASM_OUT = resolve(__dirname, '../build/wasm'); -const WASM_SRC = resolve(__dirname, '../'); +const WASM_BUILDER_CONTAINER = 'ghcr.io/nodejs/wasm-builder@\ +sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970' // v0.0.9 -if (!platform && process.argv[2]) { - platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim(); -} +const WASM_OUT = resolve(__dirname, '../build/wasm') +const WASM_SRC = resolve(__dirname, '../') -if (process.argv[2] === '--prebuild') { - const cmd = `docker build --platform=${platform.toString().trim()} -t llhttp_wasm_builder .`; +// These are defined by build environment +const WASM_CC = process.env.WASM_CC || 'clang' +let WASM_CFLAGS = process.env.WASM_CFLAGS || '--sysroot=/usr/share/wasi-sysroot -target wasm32-unknown-wasi' +let WASM_LDFLAGS = process.env.WASM_LDFLAGS || '' +const WASM_LDLIBS = process.env.WASM_LDLIBS || '' +const WASM_OPT = process.env.WASM_OPT || './wasm-opt' - console.log(`> ${cmd}\n\n`); - execSync(cmd, { stdio: 'inherit' }); +// These are relevant for undici and should not be overridden +WASM_CFLAGS += ' -Ofast -fno-exceptions -fvisibility=hidden -mexec-model=reactor' +WASM_LDFLAGS += ' -Wl,-error-limit=0 -Wl,-O3 -Wl,--lto-O3 -Wl,--strip-all' +WASM_LDFLAGS += ' -Wl,--allow-undefined -Wl,--export-dynamic -Wl,--export-table' +WASM_LDFLAGS += ' -Wl,--export=malloc -Wl,--export=free -Wl,--no-entry' - process.exit(0); -} +const WASM_OPT_FLAGS = '-O4 --converge --strip-debug --strip-dwarf --strip-producers' + +const writeWasmChunk = (path: string, dest: string) => { + const base64 = readFileSync(join(WASM_OUT, path)).toString('base64') + writeFileSync(join(WASM_OUT, dest), `'use strict' + +const { Buffer } = require('node:buffer') -if (process.argv[2] === '--setup') { - try { - mkdirSync(join(WASM_SRC, 'build')); - process.exit(0); - } catch (error: unknown) { - if (isErrorWithCode(error) && error.code !== 'EEXIST') { - throw error; - } - process.exit(0); +const wasmBase64 = '${base64}' + +let wasmBuffer + +Object.defineProperty(module, 'exports', { + get: () => { + return wasmBuffer + ? wasmBuffer + : (wasmBuffer = Buffer.from(wasmBase64, 'base64')) } +}) +`) } +let platform = process.env.WASM_PLATFORM + if (process.argv[2] === '--docker') { - let cmd = `docker run --rm -it --platform=${platform.toString().trim()}`; - // Try to avoid root permission problems on compiled assets - // when running on linux. - // It will work flawessly if uid === gid === 1000 - // there will be some warnings otherwise. + platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim() + let cmd = `docker run --rm --platform=${platform.toString().trim()} ` if (process.platform === 'linux') { - cmd += ` --user ${process.getuid!()}:${process.getegid!()}`; + cmd += ` --user ${process.getuid!()}:${process.getegid!()}` } - cmd += ` --mount type=bind,source=${WASM_SRC}/build,target=/home/node/llhttp/build llhttp_wasm_builder npm run wasm`; - console.log(`> ${cmd}\n\n`); - execSync(cmd, { cwd: WASM_SRC, stdio: 'inherit' }); - process.exit(0); + cmd += ` --mount type=bind,source=${WASM_SRC},target=/home/node/build \ + -t ${WASM_BUILDER_CONTAINER} npm run wasm` + console.log(`> ${cmd}\n\n`) + execSync(cmd, { stdio: 'inherit' }) + process.exit(0) } -try { - mkdirSync(WASM_OUT); -} catch (error: unknown) { - if (isErrorWithCode(error) && error.code !== 'EEXIST') { - throw error; +const hasApk = (function () { + try { execSync('command -v apk'); return true } catch { return false } +})() +const hasOptimizer = (function () { + try { execSync(`${WASM_OPT} --version`); return true } catch { return false } +})() +if (hasApk) { + // Gather information about the tools used for the build + const buildInfo = execSync('apk info -v').toString() + if (!buildInfo.includes('wasi-sdk')) { + console.log('Failed to generate build environment information') + process.exit(-1) } + console.log(buildInfo) } +mkdirSync(WASM_OUT, { recursive: true }) + // Build ts execSync('npm run build', { cwd: WASM_SRC, stdio: 'inherit' }); // Build wasm binary -execSync( - `clang \ - --sysroot=/usr/share/wasi-sysroot \ - -target wasm32-unknown-wasi \ - -Ofast \ - -fno-exceptions \ - -fvisibility=hidden \ - -mexec-model=reactor \ - -Wl,-error-limit=0 \ - -Wl,-O3 \ - -Wl,--lto-O3 \ - -Wl,--strip-all \ - -Wl,--allow-undefined \ - -Wl,--export-dynamic \ - -Wl,--export-table \ - -Wl,--export=malloc \ - -Wl,--export=free \ - -Wl,--no-entry \ - ${join(WASM_SRC, 'build', 'c')}/*.c \ - ${join(WASM_SRC, 'src', 'native')}/*.c \ - -I${join(WASM_SRC, 'build')} \ - -o ${join(WASM_OUT, 'llhttp.wasm')}`, - { stdio: 'inherit' }, -); +execSync(`${WASM_CC} ${WASM_CFLAGS} ${WASM_LDFLAGS} \ +${join(WASM_SRC, 'build/c')}/*.c \ +${join(WASM_SRC, 'src/native')}/*.c \ +-I${join(WASM_SRC, 'build')} \ +-o ${join(WASM_OUT, 'llhttp.wasm')} \ +${WASM_LDLIBS}`, { stdio: 'inherit' }) -// Copy constants for `.js` and `.ts` users. -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js'), join(WASM_OUT, 'constants.js')); -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js.map'), join(WASM_OUT, 'constants.js.map')); -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.d.ts'), join(WASM_OUT, 'constants.d.ts')); -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js'), join(WASM_OUT, 'utils.js')); -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js.map'), join(WASM_OUT, 'utils.js.map')); -copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.d.ts'), join(WASM_OUT, 'utils.d.ts')); - -function isErrorWithCode(error: unknown): error is Error & { code: string } { - return typeof error === 'object' && error !== null && 'code' in error; +if (hasOptimizer) { + execSync(`${WASM_OPT} ${WASM_OPT_FLAGS} \ +-o ${join(WASM_OUT, 'llhttp.wasm')} \ +${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' }) } +writeWasmChunk('llhttp.wasm', 'llhttp-wasm.js') + +// Build wasm simd binary +execSync(`${WASM_CC} ${WASM_CFLAGS} -msimd128 ${WASM_LDFLAGS} \ +${join(WASM_SRC, 'build/c')}/*.c \ +${join(WASM_SRC, 'src/native')}/*.c \ +-I${join(WASM_SRC, 'build')} \ +-o ${join(WASM_OUT, 'llhttp_simd.wasm')} \ +${WASM_LDLIBS}`, { stdio: 'inherit' }) + +if (hasOptimizer) { + execSync(`${WASM_OPT} ${WASM_OPT_FLAGS} --enable-simd \ +-o ${join(WASM_OUT, 'llhttp_simd.wasm')} \ +${join(WASM_OUT, 'llhttp_simd.wasm')}`, { stdio: 'inherit' }) +} +writeWasmChunk('llhttp_simd.wasm', 'llhttp_simd-wasm.js') + +// Copy constants for `.js` and `.ts` users. +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js'), join(WASM_OUT, 'constants.js')) +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.js.map'), join(WASM_OUT, 'constants.js.map')) +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'constants.d.ts'), join(WASM_OUT, 'constants.d.ts')) +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js'), join(WASM_OUT, 'utils.js')) +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.js.map'), join(WASM_OUT, 'utils.js.map')) +copyFileSync(join(WASM_SRC, 'lib', 'llhttp', 'utils.d.ts'), join(WASM_OUT, 'utils.d.ts')) \ No newline at end of file diff --git a/package.json b/package.json index e7909c36..b13defd7 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ "bench": "ts-node bench/", "build": "ts-node bin/generate.ts", "build-ts": "tsc", - "prebuild-wasm": "npm run wasm -- --prebuild && npm run wasm -- --setup", - "build-wasm": "npm run wasm -- --docker", + "build-wasm": "npm run build-ts && npm run wasm -- --docker", "wasm": "ts-node bin/build_wasm.ts", "clean": "rm -rf lib && rm -rf test/tmp", "prepare": "npm run clean && npm run build-ts", diff --git a/src/native/api.c b/src/native/api.c index 785891c4..02452541 100644 --- a/src/native/api.c +++ b/src/native/api.c @@ -57,29 +57,14 @@ static int wasm_on_headers_complete_wrap(llhttp_t* p) { } const llhttp_settings_t wasm_settings = { - wasm_on_message_begin, - wasm_on_url, - wasm_on_status, - NULL, - NULL, - wasm_on_header_field, - wasm_on_header_value, - NULL, - NULL, - wasm_on_headers_complete_wrap, - wasm_on_body, - wasm_on_message_complete, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, + .on_message_begin = wasm_on_message_begin, + .on_url = wasm_on_url, + .on_status = wasm_on_status, + .on_header_field = wasm_on_header_field, + .on_header_value = wasm_on_header_value, + .on_headers_complete = wasm_on_headers_complete_wrap, + .on_body = wasm_on_body, + .on_message_complete = wasm_on_message_complete, };