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: 4 additions & 0 deletions .github/actions/setup-php/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ runs:
ini-file: development
# JIT's fixed-address mappings can race Go thread creation during startup.
ini-values: opcache.jit=disable
# libenchant 2.8 can't be loaded again once PHP unloaded it, which each new start of PHP does:
# GObject still knows the types of the first load, and aborts when they come back.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this comment isn't really important, just a todo drop when patched upstream is enough

# To remove once https://github.com/php/php-src/issues/25412 is fixed in the PHP versions tested here
extensions: :enchant
coverage: none
tools: none
env:
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/sanitizers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ jobs:
env:
CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC
LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }}
CC: clang
CXX: clang++
# MemorySanitizer in clang 21, the default of Ubuntu 26.04, takes the index of an AVX-512 permutation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

also not really important to know imo

# for uninitialized memory: https://github.com/llvm/llvm-project/pull/148785
CC: clang-22
CXX: clang++-22
USE_ZEND_ALLOC: 0
LIBRARY_PATH: ${{ github.workspace }}/php/target/lib:${{ github.workspace }}/watcher/target/lib
LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib
Expand All @@ -41,6 +43,8 @@ jobs:
steps:
- name: Remove local PHP
run: sudo apt-get remove --purge --autoremove 'php*' 'libmemcached*'
- name: Install clang
run: sudo apt-get update && sudo apt-get install -y "${CC}"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
Expand All @@ -56,7 +60,7 @@ jobs:
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: php/target
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}-${{ hashFiles('.github/workflows/sanitizers.yaml') }}
- if: steps.cache-php.outputs.cache-hit != 'true'
name: Compile PHP
run: |
Expand Down
3 changes: 3 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ In your module, you need to define a new native function that will be called fro
// extension.go
package example

// #cgo linux CFLAGS: -D_GNU_SOURCE
// #include "extension.h"
import "C"
import (
Expand All @@ -694,6 +695,8 @@ func go_print_something() {

The `frankenphp.RegisterExtension()` function simplifies the extension registration process by handling the internal PHP registration logic. The `go_print_something` function uses the `//export` directive to indicate that it will be accessible in the C code we will write, thanks to CGO.

The `#cgo` line is needed on Linux: CGO includes system headers before `php.h`, and without `_GNU_SOURCE` they don't declare all the functions the PHP headers call, which recent compilers refuse.

In this example, our new function will trigger a goroutine that logs a message in Caddy's logs.

#### Define the PHP function
Expand Down
13 changes: 13 additions & 0 deletions internal/extgen/cfile_phpmethod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,19 @@ func TestCFile_ClassMethodParamCastsByParamType(t *testing.T) {
"(long)name",
},
},
{
name: "int return with nullable params points to values of the types cgo declares",
returnType: phpInt,
params: []phpParameter{
{Name: "name", PhpType: phpString, IsNullable: true},
{Name: "count", PhpType: phpInt, IsNullable: true},
{Name: "ratio", PhpType: phpFloat, IsNullable: true},
{Name: "enabled", PhpType: phpBool, IsNullable: true},
},
wantContain: []string{
"getVarInt_wrapper(intern->go_handle, name_is_null ? NULL : name, count_is_null ? NULL : &(GoInt64){count}, ratio_is_null ? NULL : &ratio, enabled_is_null ? NULL : &(GoUint8){enabled});",
},
},
}

for _, tt := range tests {
Expand Down
1 change: 1 addition & 0 deletions internal/extgen/gofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@ func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
func testGeneratedFileBasicStructure(t *testing.T, content, expectedPackage, baseName string) {
requiredElements := []string{
"package " + expectedPackage,
"// #cgo linux CFLAGS: -D_GNU_SOURCE",
"// #include <stdlib.h>",
`// #include "` + baseName + `.h"`,
`import "C"`,
Expand Down
39 changes: 38 additions & 1 deletion internal/extgen/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func TestBasicFunction(t *testing.T) {
require.NoError(t, err, "FrankenPHP compilation should succeed")

err = suite.verifyPHPSymbols(
[]string{"test_uppercase", "test_add_numbers", "test_multiply", "test_is_enabled"},
[]string{"test_uppercase", "test_add_numbers", "test_multiply", "test_is_enabled", "test_nullables"},
[]string{},
[]string{},
)
Expand Down Expand Up @@ -340,6 +340,24 @@ if ($result !== true) {
exit(1);
}

$result = test_nullables(null, null, null);
if ($result !== "null null null") {
echo "FAIL: test_nullables(null, null, null) expected 'null null null', got '$result'";
exit(1);
}

$result = test_nullables(-42, 1.5, true);
if ($result !== "-42 1.5 true") {
echo "FAIL: test_nullables(-42, 1.5, true) expected '-42 1.5 true', got '$result'";
exit(1);
}

$result = test_nullables(0, 0.0, false);
if ($result !== "0 0 false") {
echo "FAIL: test_nullables(0, 0.0, false) expected '0 0 false', got '$result'";
exit(1);
}

echo "OK";
`, "OK")
require.NoError(t, err, "all function calls should work correctly")
Expand Down Expand Up @@ -416,6 +434,25 @@ if ($counter->getValue() !== 50) {
exit(1);
}

$counter->resetIf(null);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after resetIf(null) expected 50 (unchanged), got " . $counter->getValue();
exit(1);
}

$counter->resetIf(false);
if ($counter->getValue() !== 50) {
echo "FAIL: Counter after resetIf(false) expected 50 (unchanged), got " . $counter->getValue();
exit(1);
}

$counter->resetIf(true);
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after resetIf(true) expected 0, got " . $counter->getValue();
exit(1);
}

$counter->setValue(50);
$counter->reset();
if ($counter->getValue() !== 0) {
echo "FAIL: Counter after reset expected 0, got " . $counter->getValue();
Expand Down
6 changes: 4 additions & 2 deletions internal/extgen/paramparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,14 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
switch param.PhpType {
case phpString:
return fmt.Sprintf("%s_is_null ? NULL : %s", param.Name, param.Name)
// cgo declares *int64 and *bool as GoInt64 * and GoUint8 *, which GCC 14+
// refuses to mix with zend_long * and zend_bool *: point to a copy of the right type
case phpInt:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
return fmt.Sprintf("%s_is_null ? NULL : &(GoInt64){%s}", param.Name, param.Name)
case phpFloat:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpBool:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
return fmt.Sprintf("%s_is_null ? NULL : &(GoUint8){%s}", param.Name, param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
Expand Down
4 changes: 2 additions & 2 deletions internal/extgen/paramparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
{
name: "nullable int parameter",
param: phpParameter{Name: "count", PhpType: phpInt, IsNullable: true},
expected: "count_is_null ? NULL : &count",
expected: "count_is_null ? NULL : &(GoInt64){count}",
},
{
name: "float parameter",
Expand All @@ -523,7 +523,7 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
{
name: "nullable bool parameter",
param: phpParameter{Name: "enabled", PhpType: phpBool, IsNullable: true},
expected: "enabled_is_null ? NULL : &enabled",
expected: "enabled_is_null ? NULL : &(GoUint8){enabled}",
},
{
name: "array parameter",
Expand Down
2 changes: 1 addition & 1 deletion internal/extgen/templates/extension.c.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

{{define "methodCallArg" -}}
{{- if .IsNullable -}}
{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "mixed"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}
{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &(GoInt64){ {{- .Name -}} }{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &(GoUint8){ {{- .Name -}} }{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "mixed"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}
{{- else -}}
{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "mixed"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}
{{- end -}}
Expand Down
1 change: 1 addition & 0 deletions internal/extgen/templates/extension.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package {{.PackageName}}
// You may edit the file and remove this comment if you plan to manually maintain
// this file going forward.

// #cgo linux CFLAGS: -D_GNU_SOURCE
// #include <stdlib.h>
// #include "{{.BaseName}}.h"
import "C"
Expand Down
17 changes: 17 additions & 0 deletions testdata/integration/basic_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testintegration
// #include <Zend/zend_types.h>
import "C"
import (
"strconv"
"strings"
"unsafe"

Expand Down Expand Up @@ -30,3 +31,19 @@ func test_multiply(a float64, b float64) float64 {
func test_is_enabled(flag bool) bool {
return !flag
}

// export_php:function test_nullables(?int $count, ?float $ratio, ?bool $flag): string
func test_nullables(count *int64, ratio *float64, flag *bool) unsafe.Pointer {
values := []string{"null", "null", "null"}
if count != nil {
values[0] = strconv.FormatInt(*count, 10)
}
if ratio != nil {
values[1] = strconv.FormatFloat(*ratio, 'f', -1, 64)
}
if flag != nil {
values[2] = strconv.FormatBool(*flag)
}

return frankenphp.PHPString(strings.Join(values, " "), false)
}
7 changes: 7 additions & 0 deletions testdata/integration/class_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ func (c *CounterStruct) UpdateWithNullable(newValue *int64) {
}
}

// export_php:method Counter::resetIf(?bool $condition): void
func (c *CounterStruct) ResetIf(condition *bool) {
if condition != nil && *condition {
c.Value = 0
}
}

// export_php:class StringHolder
type StringHolderStruct struct {
Data string
Expand Down
Loading