diff --git a/README.md b/README.md index 96b5275..822d3fa 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,15 @@ This may output something like (output abbreviated): Using \parallel\Runtime is easy ``` +Task Dependencies +================= + +A task receives its opcodes, captured values, and arguments. It does not inherit the submitting runtime's user-defined functions, classes, or constants. + +Functions already defined in the destination runtime, including functions loaded by its bootstrap, are linked before the task starts. Other functions are resolved when execution reaches the call, so an `include` earlier in the task can define them and an undefined-function `Error` remains catchable inside the task. + +Define required functions in the runtime bootstrap whenever possible so they can be linked before the task starts. An `include` or `require` inside a task is also supported. For dynamic dependencies, a `Closure` created with `Closure::fromCallable()` may be passed as a task argument. + Development =========== diff --git a/src/cache.c b/src/cache.c index 2c4fbe7..c428e52 100644 --- a/src/cache.c +++ b/src/cache.c @@ -175,9 +175,7 @@ static zend_always_inline void php_parallel_cache_type(zend_type *type) static zend_op_array *php_parallel_cache_create(const zend_function *source PARALLEL_CACHE_STATICS_PARAM) { zend_op_array *cached = php_parallel_cache_copy_mem((void *)source, sizeof(zend_op_array)); - uint32_t *literal_map = NULL; - uint32_t *offset_map = NULL; - uint32_t new_last_literal = cached->last_literal; + bool shared = cached->refcount == NULL; cached->fn_flags |= ZEND_ACC_IMMUTABLE; @@ -210,58 +208,19 @@ static zend_op_array *php_parallel_cache_create(const zend_function *source PARA } #endif - if (!cached->refcount) { + if (shared) { goto _php_parallel_cached_function_return; } cached->refcount = NULL; if (cached->last_literal) { - zend_op *src_opline = source->op_array.opcodes; - zend_op *src_end = src_opline + source->op_array.last; - - // A map to keep track of which literals are referenced by the - // `ZEND_INIT_FCALL` opcodes we found so that we can expand those later - literal_map = emalloc(sizeof(uint32_t) * cached->last_literal); - memset(literal_map, 0, sizeof(uint32_t) * cached->last_literal); - - // Search for `ZEND_INIT_FCALL` opcodes and remember the indexes for the - // literals, as we are rewriting them later to `ZEND_INIT_FCALL_BY_NAME` - // which requires a second, lower cased literal just in the next literal - // slot. - while (src_opline < src_end) { - if (src_opline->opcode == ZEND_INIT_FCALL && src_opline->op2_type == IS_CONST) { - uint32_t idx; -#if ZEND_USE_ABS_CONST_ADDR - idx = (zval *)src_opline->op2.zv - source->op_array.literals; -#else - idx = ((zval *)((char *)src_opline + src_opline->op2.constant) - source->op_array.literals); -#endif - if (idx < cached->last_literal) { - if (literal_map[idx] == 0) { - literal_map[idx] = 1; - new_last_literal++; - } - } - } - src_opline++; - } - } - - if (new_last_literal) { - zval *literal = source->op_array.literals; - zval *slot = php_parallel_cache_alloc(sizeof(zval) * new_last_literal); - uint32_t idx = 0; - - offset_map = emalloc(sizeof(uint32_t) * cached->last_literal); + zval *literal = source->op_array.literals, *end = literal + cached->last_literal; + zval *slot = php_parallel_cache_alloc(sizeof(zval) * cached->last_literal); cached->literals = slot; - for (uint32_t i = 0; i < cached->last_literal; i++) { - /* Record the mapping from old literal index (i) to new literal index (idx) - so we can update opcode operands later. */ - offset_map[i] = idx; - + while (literal < end) { if (Z_TYPE_P(literal) == IS_ARRAY) { ZVAL_ARR(slot, php_parallel_copy_hash_persistent(Z_ARRVAL_P(literal), php_parallel_copy_string_interned, php_parallel_cache_copy_mem, @@ -273,22 +232,9 @@ static zend_op_array *php_parallel_cache_create(const zend_function *source PARA } Z_TYPE_FLAGS_P(slot) &= ~(IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE); - - /* If this literal was used by INIT_FCALL, insert its lowercased version next. */ - if (literal_map[i]) { - zend_string *lower = zend_string_tolower(Z_STR_P(slot)); - slot++; - idx++; - ZVAL_STR(slot, php_parallel_copy_string_interned(lower)); - zend_string_release(lower); - Z_TYPE_FLAGS_P(slot) &= ~(IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE); - } - literal++; slot++; - idx++; } - cached->last_literal = new_last_literal; } if (cached->last_var) { @@ -308,44 +254,32 @@ static zend_op_array *php_parallel_cache_create(const zend_function *source PARA zend_op *opline = opcodes, *end = opline + cached->last; while (opline < end) { - /* Replace ZEND_INIT_FCALL with ZEND_INIT_FCALL_BY_NAME. - We must clear op1_type (IS_UNUSED) and op1.var (0) to invalidate the - original thread's cache slot. */ - if (opline->opcode == ZEND_INIT_FCALL) { - opline->opcode = ZEND_INIT_FCALL_BY_NAME; - opline->op1_type = IS_UNUSED; - opline->op1.var = 0; - ZEND_VM_SET_OPCODE_HANDLER(opline); - } - - /* Remap IS_CONST operands to their new locations in the expanded literal table - using the offset_map we built earlier. */ if (opline->op1_type == IS_CONST) { uint32_t idx; zend_op *src_opline = source->op_array.opcodes + (opline - opcodes); #if ZEND_USE_ABS_CONST_ADDR idx = (zval *)src_opline->op1.zv - source->op_array.literals; - opline->op1.zv = &cached->literals[offset_map[idx]]; + opline->op1.zv = &cached->literals[idx]; #else idx = ((zval *)((char *)src_opline + src_opline->op1.constant) - source->op_array.literals); - opline->op1.constant = (char *)&cached->literals[offset_map[idx]] - (char *)opline; + opline->op1.constant = (char *)&cached->literals[idx] - (char *)opline; #endif - if (opline->opcode == ZEND_SEND_VAL || opline->opcode == ZEND_SEND_VAL_EX || - opline->opcode == ZEND_QM_ASSIGN) { - zend_vm_set_opcode_handler_ex(opline, 0, 0, 0); - } } if (opline->op2_type == IS_CONST) { uint32_t idx; zend_op *src_opline = source->op_array.opcodes + (opline - opcodes); #if ZEND_USE_ABS_CONST_ADDR idx = (zval *)src_opline->op2.zv - source->op_array.literals; - opline->op2.zv = &cached->literals[offset_map[idx]]; + opline->op2.zv = &cached->literals[idx]; #else idx = ((zval *)((char *)src_opline + src_opline->op2.constant) - source->op_array.literals); - opline->op2.constant = (char *)&cached->literals[offset_map[idx]] - (char *)opline; + opline->op2.constant = (char *)&cached->literals[idx] - (char *)opline; #endif } + + if (opline->opcode == ZEND_INIT_FCALL) { + Z_EXTRA_P(RT_CONSTANT(opline, opline->op2)) = 0; + } #if ZEND_USE_ABS_JMP_ADDR switch (opline->opcode) { case ZEND_JMP: @@ -375,19 +309,13 @@ static zend_op_array *php_parallel_cache_create(const zend_function *source PARA } #endif + /* Copied opcodes cannot use handlers JIT-compiled for the source op array. */ + ZEND_VM_SET_OPCODE_HANDLER(opline); opline++; } cached->opcodes = opcodes; } - if (literal_map) { - efree(literal_map); - } - - if (offset_map) { - efree(offset_map); - } - if (cached->arg_info) { zend_arg_info *it = cached->arg_info, *end = it + cached->num_args, *info; diff --git a/src/scheduler.c b/src/scheduler.c index dfa8494..74ec951 100644 --- a/src/scheduler.c +++ b/src/scheduler.c @@ -125,6 +125,174 @@ static zend_always_inline bool php_parallel_scheduler_exit_exception(void) #endif } +typedef struct _php_parallel_scheduler_rewrite_t { + struct _php_parallel_scheduler_rewrite_t *next; + zend_op_array *op_array; + zend_op *opcodes; + zval *literals; + uint32_t last_literal; +} php_parallel_scheduler_rewrite_t; + +/* Shared cache and OPcache opcodes cannot be changed in place. */ +static void php_parallel_scheduler_clone(zend_function *function, uint32_t fallbacks, + php_parallel_scheduler_rewrite_t **rewrites) +{ + zend_op_array *op_array = &function->op_array; + zend_op *source = op_array->opcodes; + zval *source_literals = op_array->literals; + size_t header = ZEND_MM_ALIGNED_SIZE(sizeof(php_parallel_scheduler_rewrite_t)); + size_t opcodes = ZEND_MM_ALIGNED_SIZE(sizeof(zend_op) * op_array->last); + size_t literals = sizeof(zval) * (op_array->last_literal + fallbacks * 2); + php_parallel_scheduler_rewrite_t *rewrite = emalloc(header + opcodes + literals); + zend_op *copy = (zend_op *)((char *)rewrite + header); + zval *copy_literals = (zval *)((char *)copy + opcodes); + + rewrite->next = *rewrites; + rewrite->op_array = op_array; + rewrite->opcodes = source; + rewrite->literals = source_literals; + rewrite->last_literal = op_array->last_literal; + *rewrites = rewrite; + + memcpy(copy, source, sizeof(zend_op) * op_array->last); + memcpy(copy_literals, source_literals, sizeof(zval) * op_array->last_literal); + + for (uint32_t i = 0; i < op_array->last; i++) { + zend_op *opline = ©[i], *source_opline = &source[i]; + + if (opline->op1_type == IS_CONST) { + uint32_t idx; +#if ZEND_USE_ABS_CONST_ADDR + idx = (zval *)source_opline->op1.zv - source_literals; + opline->op1.zv = ©_literals[idx]; +#else + idx = ((zval *)((char *)source_opline + source_opline->op1.constant) - source_literals); + opline->op1.constant = (char *)©_literals[idx] - (char *)opline; +#endif + } + if (opline->op2_type == IS_CONST) { + uint32_t idx; +#if ZEND_USE_ABS_CONST_ADDR + idx = (zval *)source_opline->op2.zv - source_literals; + opline->op2.zv = ©_literals[idx]; +#else + idx = ((zval *)((char *)source_opline + source_opline->op2.constant) - source_literals); + opline->op2.constant = (char *)©_literals[idx] - (char *)opline; +#endif + } +#if ZEND_USE_ABS_JMP_ADDR + switch (opline->opcode) { + case ZEND_JMP: + case ZEND_FAST_CALL: + opline->op1.jmp_addr = ©[opline->op1.jmp_addr - source]; + break; +#if PHP_VERSION_ID < 80200 + case ZEND_JMPZNZ: +#endif + case ZEND_JMPZ: + case ZEND_JMPNZ: + case ZEND_JMPZ_EX: + case ZEND_JMPNZ_EX: + case ZEND_JMP_SET: + case ZEND_COALESCE: + case ZEND_FE_RESET_R: + case ZEND_FE_RESET_RW: + case ZEND_ASSERT_CHECK: + opline->op2.jmp_addr = ©[opline->op2.jmp_addr - source]; + break; + case ZEND_CATCH: + if (!(opline->extended_value & ZEND_LAST_CATCH)) { + opline->op2.jmp_addr = ©[opline->op2.jmp_addr - source]; + } + break; + } +#endif + ZEND_VM_SET_OPCODE_HANDLER(opline); + } + + op_array->opcodes = copy; + op_array->literals = copy_literals; + op_array->last_literal += fallbacks * 2; +} + +static void php_parallel_scheduler_link(zend_function *function, php_parallel_scheduler_rewrite_t **rewrites) +{ + zend_op_array *op_array = &function->op_array; + uint32_t fallbacks = 0; + bool rewrite = false; + + for (zend_op *opline = op_array->opcodes, *end = opline + op_array->last; opline < end; opline++) { + if (opline->opcode == ZEND_INIT_FCALL) { + zend_function *required = zend_fetch_function(Z_STR_P(RT_CONSTANT(opline, opline->op2))); + + if (!required || opline->op1.num != zend_vm_calc_used_stack(opline->extended_value, required)) { + fallbacks++; + rewrite = true; + } else if (required->type == ZEND_USER_FUNCTION) { + /* Function JIT may have embedded the submitter's target. */ + rewrite = true; + } + } + } + + if (rewrite) { + php_parallel_scheduler_clone(function, fallbacks, rewrites); + } + + zend_init_func_run_time_cache(op_array); + + zval *fallback = op_array->literals + op_array->last_literal - fallbacks * 2; + for (zend_op *opline = op_array->opcodes, *end = opline + op_array->last; opline < end; opline++) { + if (opline->opcode == ZEND_INIT_FCALL) { + zval *name = RT_CONSTANT(opline, opline->op2); + zend_function *required = zend_fetch_function(Z_STR_P(name)); + + if (required && opline->op1.num == zend_vm_calc_used_stack(opline->extended_value, required)) { + CACHE_PTR_EX((void **)((char *)RUN_TIME_CACHE(op_array) + opline->result.num), required); + } else { + /* INIT_FCALL names are already lower-cased function-table keys. */ + fallback[0] = *name; + fallback[1] = *name; + opline->opcode = ZEND_INIT_FCALL_BY_NAME; + opline->op1_type = IS_UNUSED; + opline->op1.var = 0; +#if ZEND_USE_ABS_CONST_ADDR + opline->op2.zv = fallback; +#else + opline->op2.constant = (char *)fallback - (char *)opline; +#endif + ZEND_VM_SET_OPCODE_HANDLER(opline); + fallback += 2; + } + } else if (opline->opcode == ZEND_DECLARE_LAMBDA_FUNCTION) { + zend_string *key; + zend_function *nested; + + PARALLEL_COPY_OPLINE_TO_FUNCTION(function, opline, &key, &nested); + php_parallel_scheduler_link(nested, rewrites); + } + } +} + +static void php_parallel_scheduler_rewrites_restore(php_parallel_scheduler_rewrite_t *rewrite) +{ + while (rewrite) { + rewrite->op_array->opcodes = rewrite->opcodes; + rewrite->op_array->literals = rewrite->literals; + rewrite->op_array->last_literal = rewrite->last_literal; + rewrite = rewrite->next; + } +} + +static void php_parallel_scheduler_rewrites_free(php_parallel_scheduler_rewrite_t *rewrite) +{ + while (rewrite) { + php_parallel_scheduler_rewrite_t *next = rewrite->next; + efree(rewrite); + rewrite = next; + } +} + static zend_always_inline void php_parallel_scheduler_kill_future(php_parallel_future_t *future) { php_parallel_monitor_lock(future->monitor); @@ -423,11 +591,12 @@ static zend_always_inline bool php_parallel_scheduler_pop(php_parallel_runtime_t static void php_parallel_scheduler_run(php_parallel_runtime_t *runtime, zend_execute_data *frame) { - php_parallel_future_t *future = php_parallel_scheduler_future; + php_parallel_future_t *future = php_parallel_scheduler_future; /* READY allows the consumer to destroy the future, so publish it only after * the worker's final access through frame->return_value. */ - volatile int32_t future_state = PHP_PARALLEL_READY; + volatile int32_t future_state = PHP_PARALLEL_READY; + php_parallel_scheduler_rewrite_t *rewrites = NULL; runtime->crashed = 0; runtime->missing = NULL; @@ -438,6 +607,8 @@ static void php_parallel_scheduler_run(php_parallel_runtime_t *runtime, zend_exe { zend_try { + php_parallel_scheduler_link(frame->func, &rewrites); + frame->opline = frame->func->op_array.opcodes; zend_execute_ex(frame); if (UNEXPECTED(EG(exception))) { @@ -513,14 +684,23 @@ static void php_parallel_scheduler_run(php_parallel_runtime_t *runtime, zend_exe } } + php_parallel_scheduler_rewrites_restore(rewrites); + php_parallel_scheduler_clean(frame->func); pefree(frame->func, 1); zend_vm_stack_free_call_frame(frame); + php_parallel_scheduler_rewrites_free(rewrites); + rewrites = NULL; } zend_end_try(); + if (rewrites) { + php_parallel_scheduler_rewrites_restore(rewrites); + php_parallel_scheduler_rewrites_free(rewrites); + } + if (future) { php_parallel_monitor_set(future->monitor, future_state); } diff --git a/tests/base/076.phpt b/tests/base/076.phpt new file mode 100644 index 0000000..58a73e9 --- /dev/null +++ b/tests/base/076.phpt @@ -0,0 +1,35 @@ +--TEST-- +JIT compiles code executed in a worker +--SKIPIF-- + +--INI-- +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.file_update_protection=0 +opcache.protect_memory=0 +--FILE-- +value(); + +var_dump(...$result); +?> +--EXPECT-- +int(2999997) +bool(true) diff --git a/tests/base/init_fcall_fix_001.phpt b/tests/base/init_fcall_fix_001.phpt index 6296bd9..a3e50b2 100644 --- a/tests/base/init_fcall_fix_001.phpt +++ b/tests/base/init_fcall_fix_001.phpt @@ -1,26 +1,22 @@ --TEST-- -Check INIT_FCALL fix with \parallel\run() (undefined function) +Missing direct function remains catchable in task --SKIPIF-- --FILE-- getMessage(); - } -})->value(); +function dummy_func(string $value): string { return $value . getenv('DUMMY_VALUE'); } + +\parallel\run(function(string $value){ + try { + return dummy_func($value); + } catch (Error $e) { + echo $e->getMessage(); + } +}, ['value'])->value(); ?> --EXPECT-- -Caught: Call to undefined function dummy_func() +Call to undefined function dummy_func() diff --git a/tests/base/init_fcall_fix_002.phpt b/tests/base/init_fcall_fix_002.phpt index e50fbbc..d92d917 100644 --- a/tests/base/init_fcall_fix_002.phpt +++ b/tests/base/init_fcall_fix_002.phpt @@ -1,27 +1,20 @@ --TEST-- -Check INIT_FCALL fix with Runtime::run() (undefined function) +Include before direct function call resolves in task --SKIPIF-- --FILE-- run(function(){ - try { - // This will be compiled as INIT_FCALL but should be converted to INIT_FCALL_BY_NAME - // and fail gracefully because it doesn't exist in the thread. - return dummy_func(); - } catch (Error $e) { - echo "Caught: " . $e->getMessage(); - } -})->value(); +echo $runtime->run(function(string $value){ + include __DIR__ . '/init_fcall_fix_002_include.inc'; + return dummy_func($value); +}, ['value'])->value(); ?> --EXPECT-- -Caught: Call to undefined function dummy_func() +worker-value diff --git a/tests/base/init_fcall_fix_002_include.inc b/tests/base/init_fcall_fix_002_include.inc new file mode 100644 index 0000000..d01c869 --- /dev/null +++ b/tests/base/init_fcall_fix_002_include.inc @@ -0,0 +1,5 @@ + --FILE-- run(function(){ - try { - $s = existing(); - // This will be compiled as INIT_FCALL but should be converted to INIT_FCALL_BY_NAME - // and fail gracefully because it doesn't exist in the thread. - $s .= dummy_func(); - $s .= dummy_func(); - return $s; - } catch (Error $e) { - echo "Caught: " . $e->getMessage(); - } -})->value(); +function dummy_func(string $value): string { return 'submitter-' . $value . getenv('DUMMY_VALUE'); } + +$runtime = new \parallel\Runtime(__DIR__ . '/init_fcall_fix_003_bootstrap.php'); +echo $runtime->run(function(string $value){ + return dummy_func($value); +}, ['value'])->value(); ?> --EXPECT-- -Caught: Call to undefined function dummy_func() +bootstrap-value diff --git a/tests/base/init_fcall_fix_003_bootstrap.php b/tests/base/init_fcall_fix_003_bootstrap.php index 3ac4fb6..adf120c 100644 --- a/tests/base/init_fcall_fix_003_bootstrap.php +++ b/tests/base/init_fcall_fix_003_bootstrap.php @@ -1,5 +1,5 @@ +--FILE-- +run(function(string $value, bool $call){ + $nested = static fn(string $value) => nested_func($value); + if ($call) { + return direct_func($value) . $nested($value); + } + return 'ok'; +}, ['value', false])->value(); +?> +--EXPECT-- +ok