Skip to content

Commit 9ec8288

Browse files
authored
1938 proposal flow executable playground (#1949)
* First PoC of working playground * Extracted wasm to standalone controller * Add autoload directly to code sample to make it explicit and return code errors in correct lines * Create flow theme for Ace Editor * Added basic autocompletion * Added error highlighting * Fixed hovering over run button * Added action buttons to playground * Cleanup logs in editor controller
1 parent 4ae8fb7 commit 9ec8288

35 files changed

+1559
-18
lines changed

composer.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,13 @@
430430
],
431431
"build:phar": [
432432
"composer update --working-dir=./src/cli",
433-
"tools/box/vendor/bin/box compile --config ./src/cli/box.json"
433+
"tools/box/vendor/bin/box compile --config ./src/cli/box.json",
434+
"cp ./build/flow.phar ./web/landing/assets/wasm/flow.phar"
435+
],
436+
"build:wasm": [
437+
"Composer\\Config::disableProcessTimeout",
438+
"cd wasm && ./build.sh",
439+
"@build:phar"
434440
],
435441
"build:docs": [
436442
"bin/docs.php dsl:dump web/landing/resources/dsl.json"

examples/topics/data_frame/data_reading/array/code.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
['id' => 4],
1515
['id' => 5],
1616
]))
17+
->withEntry('test')
1718
->collect()
1819
->write(to_stream(__DIR__ . '/output.txt', truncate: false))
1920
->run();

shell.nix

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ pkgs.mkShell {
4646
pkgs.figlet
4747
pkgs.symfony-cli
4848
pkgs.act
49+
50+
# WASM
51+
pkgs.emscripten
52+
pkgs.autoconf
53+
pkgs.wget
54+
pkgs.gnutar
55+
pkgs.xz
56+
pkgs.libxml2
57+
pkgs.pkg-config
4958
]
5059
++ pkgs.lib.optional with-blackfire pkgs.blackfire
5160
;

wasm/.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/php.wasm
2+
/php.js
3+
/php.data
4+
gh-pages
5+
6+
# Build artifacts
7+
/php-8.*
8+
/php-7.*
9+
*.phar
10+
*.phar.br
11+
manifest.json
12+
/libxml2-*
13+
14+
# Build logs
15+
*.log
16+
17+
# Nix
18+
.nix/shell/result
19+
.direnv/
20+
result
21+
22+
# Tailwind CSS
23+
/tailwindcss
24+
/input.css
25+
/tailwind.config.js
26+
27+
# Editor files
28+
.*.swp
29+
.*.swo

wasm/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

wasm/build.sh

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env bash
2+
3+
# Build script for Flow PHP Interactive Playground
4+
# Builds PHP 8.4.13
5+
6+
set -xeu
7+
8+
# Clear previous build log
9+
rm -f build.log
10+
11+
# Log all output to build.log
12+
exec > >(tee -a build.log) 2>&1
13+
echo "=== Build started at $(date) ==="
14+
15+
PHP_VERSION=8.4.13
16+
PHP_PATH=php-$PHP_VERSION
17+
18+
echo "Build libxml2 for WebAssembly"
19+
LIBXML2_VERSION=2.11.4
20+
LIBXML2_DIR=libxml2-$LIBXML2_VERSION
21+
PROJECT_ROOT=$(pwd)
22+
LIBXML2_INSTALL_DIR="$PROJECT_ROOT/$LIBXML2_DIR/install"
23+
24+
if [ ! -d "$LIBXML2_DIR" ]; then
25+
if [ ! -e $LIBXML2_DIR.tar.xz ]; then
26+
wget https://download.gnome.org/sources/libxml2/2.11/libxml2-$LIBXML2_VERSION.tar.xz
27+
fi
28+
tar xf $LIBXML2_DIR.tar.xz
29+
cd $LIBXML2_DIR
30+
31+
# Configure libxml2 for WebAssembly
32+
emconfigure ./configure \
33+
--prefix=$LIBXML2_INSTALL_DIR \
34+
--disable-shared \
35+
--enable-static \
36+
--without-python \
37+
--without-threads \
38+
--without-history \
39+
--without-readline \
40+
--without-zlib \
41+
--without-lzma
42+
43+
# Build libxml2
44+
emmake make -j$(nproc)
45+
emmake make install
46+
47+
cd $PROJECT_ROOT
48+
fi
49+
50+
echo "Download and extract PHP if needed"
51+
if [ ! -d "$PHP_PATH" ]; then
52+
if [ ! -e $PHP_PATH.tar.xz ]; then
53+
wget https://www.php.net/distributions/php-$PHP_VERSION.tar.xz
54+
fi
55+
tar xf $PHP_PATH.tar.xz
56+
fi
57+
58+
echo "Configure PHP"
59+
60+
export CFLAGS="-O3 -flto -fPIC -DZEND_MM_ERROR=0 -I$LIBXML2_INSTALL_DIR/include/libxml2 -sUSE_ZLIB=1"
61+
export LDFLAGS="-L$LIBXML2_INSTALL_DIR/lib -sUSE_ZLIB=1"
62+
63+
cd $PHP_PATH
64+
65+
# Configure with extensions required by Flow PHP
66+
# - bcmath: required by flow-php/parquet
67+
# - libxml: required as base for XML extensions
68+
# - xml, dom, xmlreader, xmlwriter: required by flow-php/etl-adapter-xml
69+
# - phar, mbstring: essential PHP extensions
70+
# - iconv: required by symfony/polyfill-mbstring
71+
72+
# Fix permissions for build scripts
73+
chmod +x buildconf build/config-stubs build/shtool 2>/dev/null || true
74+
find build -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true
75+
76+
bash ./buildconf --force
77+
78+
set +e
79+
80+
emconfigure ./configure \
81+
--disable-all \
82+
--disable-cgi \
83+
--disable-cli \
84+
--disable-rpath \
85+
--disable-phpdbg \
86+
--with-valgrind=no \
87+
--without-pear \
88+
--without-valgrind \
89+
--without-pcre-jit \
90+
--with-layout=GNU \
91+
--enable-bcmath \
92+
--enable-embed=static \
93+
--enable-phar \
94+
--enable-mbstring \
95+
--disable-mbregex \
96+
--disable-fiber-asm \
97+
--with-zlib \
98+
--with-iconv \
99+
--with-libxml \
100+
--enable-xml \
101+
--enable-dom \
102+
--enable-xmlreader \
103+
--enable-xmlwriter
104+
105+
if [ $? -ne 0 ]; then
106+
echo "emconfigure failed. Content of config.log:"
107+
cat config.log
108+
exit 1
109+
fi
110+
111+
set -e
112+
113+
echo "Build PHP"
114+
115+
emmake make clean
116+
# Use 75% of available cores
117+
cores=$(nproc)
118+
build_cores=$((cores * 3 / 4))
119+
if [ $build_cores -lt 1 ]; then
120+
build_cores=1
121+
fi
122+
echo "Using $build_cores cores (of $cores available)"
123+
emmake make -j$build_cores
124+
125+
rm -rf out
126+
mkdir -p out
127+
128+
echo "Compile pib_eval wrapper"
129+
emcc $CFLAGS -I . -I Zend -I main -I TSRM/ ../pib_eval.c -c -o pib_eval.o
130+
131+
echo "Link everything together"
132+
emcc $CFLAGS $LDFLAGS \
133+
-s ENVIRONMENT=web \
134+
-s EXPORTED_FUNCTIONS='["_pib_eval", "_pib_force_exit", "_php_embed_init", "_zend_eval_string", "_php_embed_shutdown"]' \
135+
-s EXPORTED_RUNTIME_METHODS='["ccall","FS","UTF8ToString","lengthBytesUTF8","stringToUTF8","getValue","setValue","ENV"]' \
136+
-s MODULARIZE=1 \
137+
-s EXPORT_NAME="'PHP'" \
138+
-s INITIAL_MEMORY=134217728 \
139+
-s ALLOW_MEMORY_GROWTH=1 \
140+
-s MAXIMUM_MEMORY=2147483648 \
141+
-s TOTAL_STACK=33554432 \
142+
-s STACK_SIZE=5242880 \
143+
-s ASSERTIONS=0 \
144+
-s INVOKE_RUN=0 \
145+
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
146+
-s ASYNCIFY=1 \
147+
-s STACK_OVERFLOW_CHECK=0 \
148+
-s SAFE_HEAP=0 \
149+
libs/libphp.a pib_eval.o $LIBXML2_INSTALL_DIR/lib/libxml2.a -o out/php.js
150+
151+
echo "Copy outputs to web/landing/assets/wasm"
152+
OUTPUT_DIR="$PROJECT_ROOT/../web/landing/assets/wasm"
153+
mkdir -p "$OUTPUT_DIR"
154+
cp out/php.wasm out/php.js "$OUTPUT_DIR/"
155+
156+
exit 0;

wasm/pib_eval.c

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#include "sapi/embed/php_embed.h"
2+
#include "Zend/zend_exceptions.h"
3+
#include "Zend/zend_interfaces.h"
4+
#include "Zend/zend_compile.h"
5+
#include <emscripten.h>
6+
#include <stdlib.h>
7+
#include <string.h>
8+
9+
// From Zend/zend_exceptions.c for php 7.3
10+
#if PHP_MAJOR_VERSION >= 8
11+
#define GET_PROPERTY_SILENT(object, id) \
12+
zend_read_property_ex(i_get_exception_base(object), (Z_OBJ_P(object)), ZSTR_KNOWN(id), 1, &rv)
13+
#else
14+
#define GET_PROPERTY_SILENT(object, id) \
15+
zend_read_property_ex(i_get_exception_base(object), (object), ZSTR_KNOWN(id), 1, &rv)
16+
#endif
17+
18+
// Source: php-src/sapi/php_cli.c
19+
static inline zend_class_entry *i_get_exception_base(zval *object) /* {{{ */
20+
{
21+
return instanceof_function(Z_OBJCE_P(object), zend_ce_exception) ? zend_ce_exception : zend_ce_error;
22+
}
23+
static void pib_cli_register_file_handles(void) /* {{{ */
24+
{
25+
php_stream /* *s_in, */ *s_out, *s_err;
26+
php_stream_context /* *sc_in=NULL, */ *sc_out=NULL, *sc_err=NULL;
27+
zend_constant /* ic, */ oc, ec;
28+
29+
// s_in = php_stream_open_wrapper_ex("php://stdin", "rb", 0, NULL, sc_in);
30+
s_out = php_stream_open_wrapper_ex("php://stdout", "wb", 0, NULL, sc_out);
31+
s_err = php_stream_open_wrapper_ex("php://stderr", "wb", 0, NULL, sc_err);
32+
33+
if (/* s_in == NULL || */ s_out==NULL || s_err==NULL) {
34+
// if (s_in) php_stream_close(s_in);
35+
if (s_out) php_stream_close(s_out);
36+
if (s_err) php_stream_close(s_err);
37+
return;
38+
}
39+
40+
// TODO: Support s_in
41+
// s_in_process = s_in;
42+
43+
// TODO: Set up an empty stream instead for STDIN
44+
// php_stream_to_zval(s_in, &ic.value);
45+
php_stream_to_zval(s_out, &oc.value);
46+
php_stream_to_zval(s_err, &ec.value);
47+
48+
/*
49+
ZEND_CONSTANT_SET_FLAGS(&ic, CONST_CS, 0);
50+
ic.name = zend_string_init_interned("STDIN", sizeof("STDIN")-1, 0);
51+
zend_register_constant(&ic);
52+
*/
53+
54+
ZEND_CONSTANT_SET_FLAGS(&oc, CONST_CS, 0);
55+
oc.name = zend_string_init_interned("STDOUT", sizeof("STDOUT")-1, 0);
56+
zend_register_constant(&oc);
57+
58+
ZEND_CONSTANT_SET_FLAGS(&ec, CONST_CS, 0);
59+
ec.name = zend_string_init_interned("STDERR", sizeof("STDERR")-1, 0);
60+
zend_register_constant(&ec);
61+
}
62+
/* }}} */
63+
64+
// Based on void zend_exception_error
65+
static void pib_report_exception(zend_object *ex) {
66+
// printf("exception=%llx\n", (long long)ex);
67+
zval exception;
68+
69+
ZVAL_OBJ(&exception, ex);
70+
zend_class_entry *ce_exception = Z_OBJCE(exception);
71+
72+
// Cast to string and report it.
73+
// zend_exception_error(ex, E_ERROR);
74+
if (ce_exception) {
75+
zval rv;
76+
zend_string *message = zval_get_string(GET_PROPERTY_SILENT(&exception, ZEND_STR_MESSAGE));
77+
fprintf(stderr, "Uncaught throwable '%s': %s\n", ZSTR_VAL(ce_exception->name), ZSTR_VAL(message));
78+
zend_string_release(message);
79+
zend_string *file = zval_get_string(GET_PROPERTY_SILENT(&exception, ZEND_STR_FILE));
80+
zend_long line = zval_get_long(GET_PROPERTY_SILENT(&exception, ZEND_STR_LINE));
81+
fprintf(stderr, "At %s:%d\n", ZSTR_VAL(file), line);
82+
zend_string_release(file);
83+
/*
84+
// Can't get this to work at the end of execution.
85+
if (instanceof_function(ce_exception, zend_ce_throwable)) {
86+
zval tmp;
87+
// TODO handle uncaught exception caused by __toString()
88+
zend_call_method_with_0_params(&exception, ce_exception, &ce_exception->__tostring, "__tostring", &tmp);
89+
if (Z_TYPE(tmp) == IS_STRING) {
90+
fprintf(stderr, "%s", Z_STRVAL(tmp));
91+
} else {
92+
fprintf(stderr, "Calling __toString failed\n");
93+
}
94+
zval_ptr_dtor(&tmp);
95+
}
96+
*/
97+
}
98+
}
99+
100+
// Based on code by https://github.com/oraoto/pib with modifications.
101+
int EMSCRIPTEN_KEEPALIVE pib_eval(char *code) {
102+
int ret = 0;
103+
// USE_ZEND_ALLOC prevents using fast shutdown.
104+
// putenv("USE_ZEND_ALLOC=0");
105+
php_embed_init(0, NULL);
106+
pib_cli_register_file_handles();
107+
108+
// Show fatal E_COMPILE_ERRORs and other errors properly (startup errors are normally hidden)
109+
PG(display_startup_errors)=1;
110+
PG(during_request_startup)=0;
111+
112+
// Enable error display to stdout
113+
PG(display_errors) = 1;
114+
115+
zend_first_try {
116+
// Set error_reporting to E_ALL
117+
EG(error_reporting) = E_ALL;
118+
119+
// Compile and execute the code
120+
// Use ZEND_COMPILE_POSITION_AT_OPEN_TAG to allow <?php tags
121+
zend_string *code_str = zend_string_init(code, strlen(code), 0);
122+
zend_op_array *op_array = zend_compile_string(code_str, "PIB", ZEND_COMPILE_POSITION_AT_OPEN_TAG);
123+
zend_string_release(code_str);
124+
125+
if (op_array) {
126+
zval result;
127+
ZVAL_UNDEF(&result);
128+
zend_execute(op_array, &result);
129+
zval_ptr_dtor(&result);
130+
destroy_op_array(op_array);
131+
efree(op_array);
132+
ret = SUCCESS;
133+
} else {
134+
ret = FAILURE;
135+
}
136+
137+
// If there was an uncaught error/exception, then report it.
138+
zend_object *ex = EG(exception);
139+
if (ex != NULL) {
140+
pib_report_exception(ex);
141+
}
142+
} zend_catch {
143+
zend_object *ex = EG(exception);
144+
if (ex != NULL) {
145+
EG(exception) = NULL;
146+
pib_report_exception(ex);
147+
EG(exception) = ex;
148+
}
149+
ret = EG(exit_status);
150+
} zend_end_try();
151+
php_embed_shutdown();
152+
fflush(stdout);
153+
fflush(stderr);
154+
return ret;
155+
}
156+
157+
int EMSCRIPTEN_KEEPALIVE pib_force_exit() {
158+
emscripten_force_exit(0);
159+
return 0;
160+
}

0 commit comments

Comments
 (0)