Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .dockerignore

This file was deleted.

36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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/
13 changes: 0 additions & 13 deletions Dockerfile

This file was deleted.

168 changes: 95 additions & 73 deletions bin/build_wasm.ts
Original file line number Diff line number Diff line change
@@ -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'))
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 8 additions & 23 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};


Expand Down