Skip to content

Commit abfd893

Browse files
feat: FrankenPHP extensions (#1651)
* feat: add helpers to create PHP extensions (#1644) * feat: add helpers to create PHP extensions * cs * feat: GoString * test * add test for RegisterExtension * cs * optimize includes * fix * feat(extensions): add the PHP extension generator (#1649) * feat(extensions): add the PHP extension generator * unexport many types * unexport more symbols * cleanup some tests * unexport more symbols * fix * revert types files * revert * add better validation and fix templates * remove GoStringCopy * small fixes --------- Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> * try to fix tests * fix CS * try some workarounds * try some workarounds * ingore TestRegisterExtension * exclude cgo tests in Docker images * fix * workaround... * race detector * simplify tests and code * make linter happy * feat(gofile): use templates to generate the Go file (#1666) --------- Co-authored-by: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
1 parent bbc3e49 commit abfd893

54 files changed

Lines changed: 9144 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ jobs:
197197
run: |
198198
docker run --platform=${{ matrix.platform }} --rm \
199199
"$(jq -r '."builder-${{ matrix.variant }}"."containerimage.config.digest"' <<< "${METADATA}")" \
200-
sh -c 'go test -tags ${{ matrix.race }} -v ./... && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
200+
sh -c 'go test -tags ${{ matrix.race }} -v $(go list ./... | grep -v github.com/dunglas/frankenphp/internal/testext | grep -v github.com/dunglas/frankenphp/internal/extgen) && cd caddy && go test -tags nobadger,nomysql,nopgx ${{ matrix.race }} -v ./...'
201201
env:
202202
METADATA: ${{ steps.build.outputs.metadata }}
203203
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/

.github/workflows/tests.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ jobs:
5353
- name: Build testcli binary
5454
working-directory: internal/testcli/
5555
run: go build
56+
- name: Compile library tests
57+
run: go test -race -v -x -c
5658
- name: Run library tests
57-
run: go test -race -v ./...
59+
run: ./frankenphp.test -test.v
5860
- name: Run Caddy module tests
5961
working-directory: caddy/
6062
run: go test -tags nobadger,nomysql,nopgx -race -v ./...

caddy/extinit.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package caddy
2+
3+
import (
4+
"errors"
5+
"github.com/dunglas/frankenphp/internal/extgen"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
caddycmd "github.com/caddyserver/caddy/v2/cmd"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
func init() {
16+
caddycmd.RegisterCommand(caddycmd.Command{
17+
Name: "extension-init",
18+
Usage: "go_extension.go [--verbose]",
19+
Short: "(Experimental) Initializes a PHP extension from a Go file",
20+
Long: `
21+
Initializes a PHP extension from a Go file. This command generates the necessary C files for the extension, including the header and source files, as well as the arginfo file.`,
22+
CobraFunc: func(cmd *cobra.Command) {
23+
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
24+
25+
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdInitExtension)
26+
},
27+
})
28+
}
29+
30+
func cmdInitExtension(fs caddycmd.Flags) (int, error) {
31+
if len(os.Args) < 3 {
32+
return 1, errors.New("the path to the Go source is required")
33+
}
34+
35+
sourceFile := os.Args[2]
36+
37+
baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
38+
39+
baseName = extgen.SanitizePackageName(baseName)
40+
41+
sourceDir := filepath.Dir(sourceFile)
42+
buildDir := filepath.Join(sourceDir, "build")
43+
44+
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
45+
46+
if err := generator.Generate(); err != nil {
47+
return 1, err
48+
}
49+
50+
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
51+
52+
return 0, nil
53+
}

ext.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package frankenphp
2+
3+
//#include "frankenphp.h"
4+
import "C"
5+
import (
6+
"sync"
7+
"unsafe"
8+
)
9+
10+
var (
11+
extensions []*C.zend_module_entry
12+
registerOnce sync.Once
13+
)
14+
15+
// RegisterExtension registers a new PHP extension.
16+
func RegisterExtension(me unsafe.Pointer) {
17+
extensions = append(extensions, (*C.zend_module_entry)(me))
18+
}
19+
20+
func registerExtensions() {
21+
if len(extensions) == 0 {
22+
return
23+
}
24+
25+
registerOnce.Do(func() {
26+
C.register_extensions(extensions[0], C.int(len(extensions)))
27+
extensions = nil
28+
})
29+
}

frankenphp.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,3 +1182,34 @@ int frankenphp_reset_opcache(void) {
11821182
}
11831183

11841184
int frankenphp_get_current_memory_limit() { return PG(memory_limit); }
1185+
1186+
static zend_module_entry *modules = NULL;
1187+
static int modules_len = 0;
1188+
static int (*original_php_register_internal_extensions_func)(void) = NULL;
1189+
1190+
PHPAPI int register_internal_extensions(void) {
1191+
if (original_php_register_internal_extensions_func != NULL &&
1192+
original_php_register_internal_extensions_func() != SUCCESS) {
1193+
return FAILURE;
1194+
}
1195+
1196+
for (int i = 0; i < modules_len; i++) {
1197+
if (zend_register_internal_module(&modules[i]) == NULL) {
1198+
return FAILURE;
1199+
}
1200+
}
1201+
1202+
modules = NULL;
1203+
modules_len = 0;
1204+
1205+
return SUCCESS;
1206+
}
1207+
1208+
void register_extensions(zend_module_entry *m, int len) {
1209+
modules = m;
1210+
modules_len = len;
1211+
1212+
original_php_register_internal_extensions_func =
1213+
php_register_internal_extensions_func;
1214+
php_register_internal_extensions_func = register_internal_extensions;
1215+
}

frankenphp.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ func Init(options ...Option) error {
226226
// Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90
227227
signal.Ignore(syscall.SIGPIPE)
228228

229+
registerExtensions()
230+
229231
opt := &opt{}
230232
for _, o := range options {
231233
if err := o(opt); err != nil {

frankenphp.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef _FRANKENPPHP_H
22
#define _FRANKENPPHP_H
33

4+
#include <Zend/zend_modules.h>
45
#include <Zend/zend_types.h>
56
#include <stdbool.h>
67
#include <stdint.h>
@@ -92,4 +93,6 @@ void frankenphp_register_bulk(
9293
ht_key_value_pair auth_type, ht_key_value_pair remote_ident,
9394
ht_key_value_pair request_uri);
9495

96+
void register_extensions(zend_module_entry *m, int len);
97+
9598
#endif

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
retract v1.0.0-rc.1 // Human error
66

77
require (
8+
github.com/Masterminds/sprig/v3 v3.3.0
89
github.com/maypok86/otter v1.2.4
910
github.com/prometheus/client_golang v1.22.0
1011
github.com/stretchr/testify v1.10.0
@@ -14,19 +15,29 @@ require (
1415
)
1516

1617
require (
18+
dario.cat/mergo v1.0.1 // indirect
19+
github.com/Masterminds/goutils v1.1.1 // indirect
20+
github.com/Masterminds/semver/v3 v3.3.0 // indirect
1721
github.com/beorn7/perks v1.0.1 // indirect
1822
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1923
github.com/davecgh/go-spew v1.1.1 // indirect
2024
github.com/dolthub/maphash v0.1.0 // indirect
2125
github.com/gammazero/deque v1.0.0 // indirect
26+
github.com/google/uuid v1.6.0 // indirect
27+
github.com/huandu/xstrings v1.5.0 // indirect
2228
github.com/kylelemons/godebug v1.1.0 // indirect
29+
github.com/mitchellh/copystructure v1.2.0 // indirect
30+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
2331
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2432
github.com/pmezard/go-difflib v1.0.0 // indirect
2533
github.com/prometheus/client_model v0.6.2 // indirect
2634
github.com/prometheus/common v0.64.0 // indirect
2735
github.com/prometheus/procfs v0.16.1 // indirect
2836
github.com/rogpeppe/go-internal v1.12.0 // indirect
37+
github.com/shopspring/decimal v1.4.0 // indirect
38+
github.com/spf13/cast v1.7.0 // indirect
2939
go.uber.org/multierr v1.11.0 // indirect
40+
golang.org/x/crypto v0.39.0 // indirect
3041
golang.org/x/sys v0.33.0 // indirect
3142
golang.org/x/text v0.26.0 // indirect
3243
google.golang.org/protobuf v1.36.6 // indirect

go.sum

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
2+
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3+
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
4+
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
5+
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
6+
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
7+
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
8+
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
19
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
210
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
311
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -6,10 +14,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
614
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
715
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
816
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
17+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
18+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
919
github.com/gammazero/deque v1.0.0 h1:LTmimT8H7bXkkCy6gZX7zNLtkbz4NdS2z8LZuor3j34=
1020
github.com/gammazero/deque v1.0.0/go.mod h1:iflpYvtGfM3U8S8j+sZEKIak3SAKYpA5/SQewgfXDKo=
1121
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1222
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
23+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
24+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
25+
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
26+
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
1327
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
1428
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
1529
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -18,6 +32,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
1832
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
1933
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
2034
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
35+
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
36+
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
37+
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
38+
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
2139
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
2240
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
2341
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -32,6 +50,10 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
3250
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
3351
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
3452
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
53+
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
54+
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
55+
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
56+
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
3557
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3658
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
3759
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -42,6 +64,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
4264
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
4365
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
4466
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
67+
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
68+
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
4569
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
4670
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
4771
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=

internal/extgen/arginfo.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package extgen
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strings"
9+
)
10+
11+
type arginfoGenerator struct {
12+
generator *Generator
13+
}
14+
15+
func (ag *arginfoGenerator) generate() error {
16+
genStubPath := os.Getenv("GEN_STUB_SCRIPT")
17+
if genStubPath == "" {
18+
genStubPath = "/usr/local/src/php/build/gen_stub.php"
19+
}
20+
21+
if _, err := os.Stat(genStubPath); err != nil {
22+
return fmt.Errorf(`the PHP "gen_stub.php" file couldn't be found under %q, you can set the "GEN_STUB_SCRIPT" environement variable to set a custom location`, genStubPath)
23+
}
24+
25+
stubFile := ag.generator.BaseName + ".stub.php"
26+
cmd := exec.Command("php", genStubPath, filepath.Join(ag.generator.BuildDir, stubFile))
27+
28+
if err := cmd.Run(); err != nil {
29+
return fmt.Errorf("running gen_stub script: %w", err)
30+
}
31+
32+
return ag.fixArginfoFile(stubFile)
33+
}
34+
35+
func (ag *arginfoGenerator) fixArginfoFile(stubFile string) error {
36+
arginfoFile := strings.TrimSuffix(stubFile, ".stub.php") + "_arginfo.h"
37+
arginfoPath := filepath.Join(ag.generator.BuildDir, arginfoFile)
38+
39+
content, err := ReadFile(arginfoPath)
40+
if err != nil {
41+
return fmt.Errorf("reading arginfo file: %w", err)
42+
}
43+
44+
// FIXME: the script generate "zend_register_internal_class_with_flags" but it is not recognized by the compiler
45+
fixedContent := strings.ReplaceAll(content,
46+
"zend_register_internal_class_with_flags(&ce, NULL, 0)",
47+
"zend_register_internal_class(&ce)")
48+
49+
return WriteFile(arginfoPath, fixedContent)
50+
}

0 commit comments

Comments
 (0)