Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ jobs:

git fetch --no-tags --depth=1 origin "$base_sha"

if git diff --name-only "$base_sha" "$GITHUB_SHA" -- packages/php-wasm/compile-extension/ | grep -q .; then
if git diff --name-only "$base_sha" "$GITHUB_SHA" -- packages/php-wasm/compile-extension/ packages/php-wasm/compile/php/ | grep -q .; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense.

echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -541,6 +541,8 @@ jobs:
run: node --expose-gc node_modules/nx/bin/nx run php-wasm-compile-extension:test-lazy-docker-asset-fetch --output-style=stream
- name: Build and load PHP.wasm extension fixtures
run: node --expose-gc node_modules/nx/bin/nx affected --target=test-compile-extension-integration --parallel=1 --output-style=stream
- name: Load external extension against freshly built PHP 8.5 JSPI runtime
run: node --expose-gc node_modules/nx/bin/nx run php-wasm-compile-extension:test-external-extension-abi --output-style=stream
Comment thread
brandonpayton marked this conversation as resolved.

test-legacy-wp-version-boot:
if: github.repository == 'WordPress/wordpress-playground' || github.event_name == 'pull_request'
Expand Down
13 changes: 13 additions & 0 deletions packages/php-wasm/compile-extension/docker/Dockerfile.ext
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ RUN source /root/emsdk/emsdk_env.sh && \
cd /root/php-src && \
emmake make install

# PHP optimizes constant-size emalloc() calls into build-specific
# _emalloc_<size>() symbols. External extensions are built independently from
# PHP.wasm runtimes, so use the stable _emalloc() entry point instead of
# coupling extension binaries to a runtime's allocator-specific shortcuts.
RUN if ! grep -Fqx '#undef HAVE_BUILTIN_CONSTANT_P' \
/usr/local/include/php/Zend/zend_alloc.h; then \
/root/replace.sh \
's/#include "zend_alloc_sizes.h"/#include "zend_alloc_sizes.h"\n#undef HAVE_BUILTIN_CONSTANT_P/' \
/usr/local/include/php/Zend/zend_alloc.h; \
fi && \
grep -Fqx '#undef HAVE_BUILTIN_CONSTANT_P' \
/usr/local/include/php/Zend/zend_alloc.h

RUN mkdir -p /usr/local/include/php/ext/standard && \
if [ -f /root/php-src/ext/standard/php_smart_string.h ]; then \
cp /root/php-src/ext/standard/*.h /usr/local/include/php/ext/standard/; \
Expand Down
6 changes: 6 additions & 0 deletions packages/php-wasm/compile-extension/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
"command": "packages/php-wasm/compile-extension/tests/run-integration-tests.sh"
}
},
"test-external-extension-abi": {
"executor": "nx:run-commands",
"options": {
"command": "packages/php-wasm/compile-extension/tests/test-external-extension-abi.sh"
Comment thread
brandonpayton marked this conversation as resolved.
}
},
"test-lazy-docker-asset-fetch": {
"executor": "nx:run-commands",
"options": {
Expand Down
17 changes: 17 additions & 0 deletions packages/php-wasm/compile-extension/scripts/build-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,22 @@ if [ -x /root/emsdk/upstream/bin/wasm-opt ]; then
-o "$module_path"
fi

node --input-type=module - "$module_path" <<'EOF'
import { readFile } from 'node:fs/promises';

const modulePath = process.argv[2];
const module = new WebAssembly.Module(await readFile(modulePath));
const hasGetModule = WebAssembly.Module.exports(module).some(
({ kind, name }) => kind === 'function' && name === 'get_module'
);

if (!hasGetModule) {
throw new Error(
`${modulePath} does not export PHP's get_module() entry point. ` +
'PHP extensions must include config.h and call ZEND_GET_MODULE().'
Comment thread
brandonpayton marked this conversation as resolved.
);
}
EOF

mkdir -p /out
cp "$module_path" "/out/${ARTIFACT_FILENAME}"
26 changes: 26 additions & 0 deletions packages/php-wasm/compile-extension/src/compile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';

// eslint-disable-next-line @nx/enforce-module-boundaries
import { phpVersions } from '../../supported-php-versions.mjs';
import { resolvePHPRelease, SupportedExtensionPHPVersions } from './compile';

describe('resolvePHPRelease', () => {
it('uses the canonical release for every supported extension PHP minor version', () => {
const canonicalReleases = new Map(
phpVersions.map(({ version, lastRelease }) => [
version,
lastRelease,
])
);

for (const phpVersion of SupportedExtensionPHPVersions) {
expect(resolvePHPRelease(phpVersion)).toBe(
canonicalReleases.get(phpVersion)
);
}
});

it('resolves PHP 8.4 to the canonical release', () => {
expect(resolvePHPRelease('8.4')).toBe('8.4.23');
});
});
21 changes: 9 additions & 12 deletions packages/php-wasm/compile-extension/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { mkdir, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

// This repository-level file is the canonical source used by PHP builds.
// eslint-disable-next-line @nx/enforce-module-boundaries
import { phpVersions } from '../../supported-php-versions.mjs';

import {
assertDockerIsAvailable,
buildBaseImage,
Expand All @@ -26,16 +30,6 @@ export const SupportedExtensionPHPVersions = [
'7.4',
] as const;

const PHP_RELEASE_BY_MINOR: Record<string, string> = {
'8.5': '8.5.5',
'8.4': '8.4.20',
'8.3': '8.3.30',
'8.2': '8.2.30',
'8.1': '8.1.34',
'8.0': '8.0.30',
'7.4': '7.4.33',
};

export interface CompileExtensionOptions {
workspaceRoot: string;
sourceDir: string;
Expand Down Expand Up @@ -99,7 +93,7 @@ export async function compileExtensionMatrix(options: CompileExtensionOptions) {
options.phpVersions.map((phpVersion) => ({
phpVersion,
asyncMode: ExtensionAsyncMode,
}));
}));

await mkdir(outDir, { recursive: true });
await buildBaseImage(context);
Expand Down Expand Up @@ -149,7 +143,10 @@ export async function compileExtensionMatrix(options: CompileExtensionOptions) {
}

export function resolvePHPRelease(phpVersion: string): string {
return PHP_RELEASE_BY_MINOR[phpVersion] ?? phpVersion;
return (
phpVersions.find(({ version }) => version === phpVersion)
?.lastRelease ?? phpVersion
);
Comment thread
brandonpayton marked this conversation as resolved.
}

async function detectManifestVersion(sourceDir: string): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PHP_ARG_ENABLE([external_abi], [whether to enable external_abi], [--enable-external_abi])

if test "$PHP_EXTERNAL_ABI" != "no"; then
PHP_NEW_EXTENSION([external_abi], [external_abi.c], [$ext_shared])
fi
Comment thread
chubes4 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "ext/standard/php_password.h"
#include <stdio.h>
#include <string.h>
#include <strings.h>

PHP_FUNCTION(external_abi_probe)
{
zval value;
char cleared[4] = {1, 1, 1, 1};
int parsed = 0;
void *first = emalloc(160);
void *second = emalloc(448);
zend_string *algorithm = zend_string_init("bcrypt", sizeof("bcrypt") - 1, 0);
zend_string *mixed_case = zend_string_init("Zstd", sizeof("Zstd") - 1, 0);
int compared;

ZVAL_LONG(&value, 1);
convert_to_null(&value);
php_password_algo_find(algorithm);
php_password_algo_register("external_abi", &php_password_algo_bcrypt);
explicit_bzero(cleared, sizeof(cleared));
sscanf("42", "%d", &parsed);
compared = strncasecmp(ZSTR_VAL(mixed_case), "zstd", 4);
zend_string_release(algorithm);
zend_string_release(mixed_case);
efree(first);
efree(second);
RETURN_BOOL(cleared[0] == 0 && parsed == 42 && compared == 0);
}

static const zend_function_entry external_abi_functions[] = {
PHP_FE(external_abi_probe, NULL)
PHP_FE_END
};

zend_module_entry external_abi_module_entry = {
STANDARD_MODULE_HEADER,
"external_abi",
external_abi_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
"0.1.0",
STANDARD_MODULE_PROPERTIES,
};

#ifdef COMPILE_DL_EXTERNAL_ABI
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(external_abi)
#endif
68 changes: 52 additions & 16 deletions packages/php-wasm/compile-extension/tests/load-built-extension.mjs
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import { loadNodeRuntime } from '@php-wasm/node';
import { PHP } from '@php-wasm/universal';
import {
loadPHPRuntime,
PHP,
resolvePHPExtension,
withResolvedPHPExtensions,
} from '@php-wasm/universal';
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';

const [manifestPath, phpVersion, code, expectedOutput] = process.argv.slice(2);
const [manifestPath, phpVersion, code, expectedOutput, runtimeLoaderPath] =
process.argv.slice(2);

if (!manifestPath || !phpVersion || !code) {
throw new Error(
'Usage: load-built-extension.mjs <manifest> <php-version> <php-code> <expected-output>'
'Usage: load-built-extension.mjs <manifest> <php-version> <php-code> <expected-output> [runtime-loader]'
);
}

const php = new PHP(
await loadNodeRuntime(phpVersion, {
emscriptenOptions: { processId: 1 },
extensions: [
{
source: {
format: 'manifest',
manifestUrl: manifestPath,
},
},
],
})
);
const php = runtimeLoaderPath
? await loadFreshRuntime(manifestPath, phpVersion, runtimeLoaderPath)
Comment thread
brandonpayton marked this conversation as resolved.
: new PHP(
await loadNodeRuntime(phpVersion, {
emscriptenOptions: { processId: 1 },
extensions: [
{
source: {
format: 'manifest',
manifestUrl: manifestPath,
},
},
],
})
);
try {
const result = await php.run({ code });
if (result.errors) {
Expand All @@ -37,3 +47,29 @@ try {
} finally {
php.exit();
}

async function loadFreshRuntime(manifestPath, phpVersion, runtimeLoaderPath) {
const runtimeModule = await import(pathToFileURL(runtimeLoaderPath).href);
const extension = await resolvePHPExtension({
source: {
format: 'manifest',
manifestUrl: pathToFileURL(manifestPath).href,
},
phpVersion,
fetch: async (url) => {
const response =
new URL(url).protocol === 'file:'
? new Response(await readFile(new URL(url)))
: await fetch(url);
if (!response.ok) {
throw new Error(`Could not load extension artifact: ${url}`);
}
return response;
},
});
const runtimeId = await loadPHPRuntime(
runtimeModule,
withResolvedPHPExtensions({ phpWasmAsyncMode: 'jspi' }, [extension])
);
return new PHP(runtimeId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
Comment thread
brandonpayton marked this conversation as resolved.
set -euo pipefail

ROOT_DIR="$(git rev-parse --show-toplevel)"
PHP_VERSION="${PHP_VERSION:-8.5}"

if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required for external extension ABI tests." >&2
exit 1
fi

mkdir -p "${ROOT_DIR}/tmp"
WORK_DIR="$(mktemp -d "${ROOT_DIR}/tmp/external-extension-abi.XXXXXX")"
trap 'rm -rf "$WORK_DIR"' EXIT

node "$ROOT_DIR/packages/php-wasm/compile/build.js" \
--PLATFORM=node \
--PHP_VERSION="$PHP_VERSION" \
--WITH_JSPI=yes \
--output-dir="$WORK_DIR/runtime"

node \
--experimental-wasm-jspi \
--experimental-strip-types \
--experimental-transform-types \
--disable-warning=ExperimentalWarning \
--import "$ROOT_DIR/packages/meta/src/node-es-module-loader/register.mts" \
"$ROOT_DIR/packages/php-wasm/compile-extension/src/cli.ts" \
--source "$ROOT_DIR/packages/php-wasm/compile-extension/tests/fixtures/external-abi" \
--name external_abi \
--php-versions "$PHP_VERSION" \
--jobs 1 \
--out "$WORK_DIR/extension"

node \
--experimental-wasm-jspi \
--experimental-strip-types \
--experimental-transform-types \
--disable-warning=ExperimentalWarning \
--import "$ROOT_DIR/packages/meta/src/node-es-module-loader/register.mts" \
"$ROOT_DIR/packages/php-wasm/compile-extension/tests/load-built-extension.mjs" \
"$WORK_DIR/extension/manifest.json" \
"$PHP_VERSION" \
"<?php echo external_abi_probe() ? 'external ABI loaded' : 'probe failed';" \
"external ABI loaded" \
"$WORK_DIR/runtime/php_${PHP_VERSION//./_}.js"
22 changes: 20 additions & 2 deletions packages/php-wasm/compile/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ fi
COPY ./compile/libzip/ /root/builds-libzip/
RUN mkdir -p /libs/libzip
RUN if [ "$WITH_JSPI" = "yes" ]; then \
cp -r /root/builds-libzip/jspi/dist/1.2.0/root/lib /libs/libzip/1.2.0; \
cp -r /root/builds-libzip/jspi/dist/1.9.2/root/lib /libs/libzip/1.9.2; \
else \
cp -r /root/builds-libzip/asyncify/dist/1.2.0/root/lib /libs/libzip/1.2.0; \
cp -r /root/builds-libzip/asyncify/dist/1.9.2/root/lib /libs/libzip/1.9.2; \
fi

Expand Down Expand Up @@ -2188,6 +2186,26 @@ COPY ./compile/php/exported-functions.list /root/exported-functions.list
COPY ./compile/php/zend-side-module-exports.c /root/zend-side-module-exports.c
RUN cat /root/exported-functions.list >> /root/.JS_ABI_EXPORTS

# External phpize extensions are not available while this main module is linked.
# Export the complete public symbol surface of the matching libphp archive so
# MAIN_MODULE=2 can retain only the PHP extension ABI instead of every symbol.
# EXPORTED_FUNCTIONS uses Emscripten's JS-facing leading underscore, so a PHP
# symbol such as _emalloc intentionally becomes __emalloc in that list.
RUN set -eux; \
/root/emsdk/upstream/bin/llvm-nm \
--defined-only \
--extern-only \
--format=just-symbols \
/root/lib/libphp.a \
| grep -v '^[[:space:]]*$' \
| grep -v ':$' \
| LC_ALL=C sort -u \
> /root/.PHP_EXTENSION_ABI_EXPORTS; \
sed 's/^/_/' /root/.PHP_EXTENSION_ABI_EXPORTS \
>> /root/.JS_ABI_EXPORTS; \
cat /root/.PHP_EXTENSION_ABI_EXPORTS \
>> /root/.WASM_ABI_EXPORTS
Comment thread
chubes4 marked this conversation as resolved.
Comment thread
chubes4 marked this conversation as resolved.

RUN set -eux; \
if [ "$WITH_JSPI" != "yes" ]; then \
exit 0; \
Expand Down
Loading