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
13 changes: 12 additions & 1 deletion docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,18 @@ GEN_STUB_SCRIPT=php-src/build/gen_stub.php frankenphp extension-init my_extensio
> [!NOTE]
> Don't forget to set the `GEN_STUB_SCRIPT` environment variable to the path of the `gen_stub.php` file in the PHP sources you downloaded earlier. This is the same `gen_stub.php` script mentioned in the manual implementation section.

If everything went well, a new directory named `build` should have been created. This directory contains the generated files for your extension, including the `my_extension.go` file with the generated PHP function stubs.
If everything went well, your project directory should contain the following files for your extension:

- **`my_extension.go`** - Your original source file (remains unchanged)
- **`my_extension_generated.go`** - Generated file with CGO wrappers that call your functions
- **`my_extension.stub.php`** - PHP stub file for IDE autocompletion
- **`my_extension_arginfo.h`** - PHP argument information
- **`my_extension.h`** - C header file
- **`my_extension.c`** - C implementation file
- **`README.md`** - Documentation

> [!IMPORTANT]
> **Your source file (`my_extension.go`) is never modified.** The generator creates a separate `_generated.go` file containing CGO wrappers that call your original functions. This means you can safely version control your source file without worrying about generated code polluting it.

### Integrating the Generated Extension into FrankenPHP

Expand Down
171 changes: 134 additions & 37 deletions internal/extgen/gofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"bytes"
_ "embed"
"fmt"
"os"
"go/format"
"path/filepath"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
Expand All @@ -21,7 +22,7 @@ type GoFileGenerator struct {
type goTemplateData struct {
PackageName string
BaseName string
Imports []string
SanitizedBaseName string
Constants []phpConstant
Variables []string
InternalFunctions []string
Expand All @@ -30,16 +31,7 @@ type goTemplateData struct {
}

func (gg *GoFileGenerator) generate() error {
filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+".go")

if _, err := os.Stat(filename); err == nil {
backupFilename := filename + ".bak"
if err := os.Rename(filename, backupFilename); err != nil {
return fmt.Errorf("backing up existing Go file: %w", err)
}

gg.generator.SourceFile = backupFilename
}
filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+"_generated.go")

content, err := gg.buildContent()
if err != nil {
Expand All @@ -51,38 +43,18 @@ func (gg *GoFileGenerator) generate() error {

func (gg *GoFileGenerator) buildContent() (string, error) {
sourceAnalyzer := SourceAnalyzer{}
imports, variables, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile)
packageName, variables, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile)
if err != nil {
return "", fmt.Errorf("analyzing source file: %w", err)
}

filteredImports := make([]string, 0, len(imports))
for _, imp := range imports {
if imp != `"C"` && imp != `"unsafe"` && imp != `"github.com/dunglas/frankenphp"` && imp != `"runtime/cgo"` {
filteredImports = append(filteredImports, imp)
}
}

classes := make([]phpClass, len(gg.generator.Classes))
copy(classes, gg.generator.Classes)

if len(classes) > 0 {
hasCgo := false
for _, imp := range imports {
if imp == `"runtime/cgo"` {
hasCgo = true
break
}
}
if !hasCgo {
filteredImports = append(filteredImports, `"runtime/cgo"`)
}
}

templateContent, err := gg.getTemplateContent(goTemplateData{
PackageName: SanitizePackageName(gg.generator.BaseName),
PackageName: packageName,
BaseName: gg.generator.BaseName,
Imports: filteredImports,
SanitizedBaseName: SanitizePackageName(gg.generator.BaseName),
Constants: gg.generator.Constants,
Variables: variables,
InternalFunctions: internalFunctions,
Expand All @@ -94,7 +66,12 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
return "", fmt.Errorf("executing template: %w", err)
}

return templateContent, nil
fc, err := format.Source([]byte(templateContent))
if err != nil {
return "", fmt.Errorf("formatting source: %w", err)
}

return string(fc), nil
}

func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) {
Expand All @@ -106,6 +83,10 @@ func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, erro
funcMap["isVoid"] = func(t phpType) bool {
return t == phpVoid
}
funcMap["extractGoFunctionName"] = extractGoFunctionName
funcMap["extractGoFunctionSignatureParams"] = extractGoFunctionSignatureParams
funcMap["extractGoFunctionSignatureReturn"] = extractGoFunctionSignatureReturn
funcMap["extractGoFunctionCallParams"] = extractGoFunctionCallParams

tmpl := template.Must(template.New("gofile").Funcs(funcMap).Parse(goFileContent))

Expand All @@ -128,7 +109,7 @@ type GoParameter struct {
Type string
}

var phpToGoTypeMap= map[phpType]string{
var phpToGoTypeMap = map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
Expand All @@ -146,3 +127,119 @@ func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {

return "any"
}

// extractGoFunctionName extracts the Go function name from a Go function signature string.
func extractGoFunctionName(goFunction string) string {
idx := strings.Index(goFunction, "func ")
if idx == -1 {
return ""
}

start := idx + len("func ")

end := start
for end < len(goFunction) && goFunction[end] != '(' {
end++
}

if end >= len(goFunction) {
return ""
}

return strings.TrimSpace(goFunction[start:end])
}

// extractGoFunctionSignatureParams extracts the parameters from a Go function signature.
func extractGoFunctionSignatureParams(goFunction string) string {
start := strings.IndexByte(goFunction, '(')
if start == -1 {
return ""
}
start++

depth := 1
end := start
for end < len(goFunction) && depth > 0 {
switch goFunction[end] {
case '(':
depth++
case ')':
depth--
}
if depth > 0 {
end++
}
}

if end >= len(goFunction) {
return ""
}

return strings.TrimSpace(goFunction[start:end])
}

// extractGoFunctionSignatureReturn extracts the return type from a Go function signature.
func extractGoFunctionSignatureReturn(goFunction string) string {
start := strings.IndexByte(goFunction, '(')
if start == -1 {
return ""
}

depth := 1
pos := start + 1
for pos < len(goFunction) && depth > 0 {
switch goFunction[pos] {
case '(':
depth++
case ')':
depth--
}
pos++
}

if pos >= len(goFunction) {
return ""
}

end := strings.IndexByte(goFunction[pos:], '{')
if end == -1 {
return ""
}
end += pos

returnType := strings.TrimSpace(goFunction[pos:end])
return returnType
}

// extractGoFunctionCallParams extracts just the parameter names for calling a function.
func extractGoFunctionCallParams(goFunction string) string {
params := extractGoFunctionSignatureParams(goFunction)
if params == "" {
return ""
}

var names []string
parts := strings.Split(params, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if len(part) == 0 {
continue
}

words := strings.Fields(part)
if len(words) > 0 {
names = append(names, words[0])
}
}

var result strings.Builder
for i, name := range names {
if i > 0 {
result.WriteString(", ")
}

result.WriteString(name)
}

return result.String()
}
Loading
Loading