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
27 changes: 27 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,33 @@ $user->updateInfo(null, 25, null); // Name and active are null

This design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety.

### Module Initialization and Shutdown

The generator supports defining module initialization and shutdown functions using the `//export_php:module` directive.
This allows you to perform setup and cleanup operations when your extension is loaded and unloaded.

To define an initialization function, tag it with `//export_php:module init`:

```go
//export_php:module init
func initializeModule() {
// Perform initialization tasks
// For example, set up global resources, initialize data structures, etc.
}
```

To define a shutdown function, tag it with `//export_php:module shutdown`:

```go
//export_php:module shutdown
func cleanupModule() {
// Perform cleanup tasks
// For example, free resources, close connections, etc.
}
```

You can define either one, both, or none of these functions. The initialization function will be called when the PHP module is loaded, and the shutdown function will be called when the PHP module is unloaded.

### Declaring Constants

The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconstant` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code.
Expand Down
2 changes: 2 additions & 0 deletions internal/extgen/cfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type cTemplateData struct {
Classes []phpClass
Constants []phpConstant
Namespace string
Module *phpModule
}

func (cg *cFileGenerator) generate() error {
Expand Down Expand Up @@ -68,6 +69,7 @@ func (cg *cFileGenerator) getTemplateContent() (string, error) {
Classes: cg.generator.Classes,
Constants: cg.generator.Constants,
Namespace: cg.generator.Namespace,
Module: cg.generator.Module,
}); err != nil {
return "", err
}
Expand Down
7 changes: 7 additions & 0 deletions internal/extgen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Generator struct {
Classes []phpClass
Constants []phpConstant
Namespace string
Module *phpModule
}

// EXPERIMENTAL
Expand Down Expand Up @@ -86,6 +87,12 @@ func (g *Generator) parseSource() error {
}
g.Namespace = ns

module, err := parser.ParseModule(g.SourceFile)
if err != nil {
return fmt.Errorf("parsing module: %w", err)
}
g.Module = module

return nil
}

Expand Down
15 changes: 15 additions & 0 deletions internal/extgen/gofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
_ "embed"
"fmt"
"path/filepath"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
Expand All @@ -25,6 +26,8 @@ type goTemplateData struct {
InternalFunctions []string
Functions []phpFunction
Classes []phpClass
Module *phpModule
HasInitFunction bool
}

func (gg *GoFileGenerator) generate() error {
Expand Down Expand Up @@ -54,6 +57,16 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
classes := make([]phpClass, len(gg.generator.Classes))
copy(classes, gg.generator.Classes)

// Check if there's already an init() function in the source file
hasInitFunction := false
for _, fn := range internalFunctions {
if strings.HasPrefix(fn, "func init()") {
hasInitFunction = true
fmt.Printf("Warning: An init() function already exists in the source file. Make sure to call frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry)) in your init function.\n")
break
}
}

templateContent, err := gg.getTemplateContent(goTemplateData{
PackageName: SanitizePackageName(gg.generator.BaseName),
BaseName: gg.generator.BaseName,
Expand All @@ -62,6 +75,8 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
InternalFunctions: internalFunctions,
Functions: gg.generator.Functions,
Classes: classes,
Module: gg.generator.Module,
HasInitFunction: hasInitFunction,
})

if err != nil {
Expand Down
178 changes: 175 additions & 3 deletions internal/extgen/gofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ func test() {
{
Name: "test",
ReturnType: phpVoid,
GoFunction: "func test() {\n\t// simple function\n}",
GoFunction: `func test() {
// simple function
}`,
},
},
contains: []string{
Expand Down Expand Up @@ -213,7 +215,9 @@ func TestGoFileGenerator_PackageNameSanitization(t *testing.T) {

for _, tt := range tests {
t.Run(tt.baseName, func(t *testing.T) {
sourceFile := createTempSourceFile(t, "package main\n//export_php: test(): void\nfunc test() {}")
sourceFile := createTempSourceFile(t, `package main
//export_php: test(): void
func test() {}`)

generator := &Generator{
BaseName: tt.baseName,
Expand Down Expand Up @@ -251,7 +255,8 @@ func TestGoFileGenerator_ErrorHandling(t *testing.T) {
},
{
name: "valid file",
sourceFile: createTempSourceFile(t, "package main\nfunc test() {}"),
sourceFile: createTempSourceFile(t, `package main
func test() {}`),
expectErr: false,
},
}
Expand Down Expand Up @@ -732,3 +737,170 @@ func testGoFileInternalFunctions(t *testing.T, content string) {
t.Log("No internal functions found (this may be expected)")
}
}

func TestGoFileGenerator_RuntimeCgoImportGating(t *testing.T) {
tests := []struct {
name string
baseName string
sourceFile string
functions []phpFunction
classes []phpClass
expectCgoImport bool
description string
}{
{
name: "extension without classes should not include runtime/cgo import",
baseName: "no_classes",
sourceFile: createTempSourceFile(t, `package main

//export_php: simpleFunc(): void
func simpleFunc() {
// simple function
}`),
functions: []phpFunction{
{
Name: "simpleFunc",
ReturnType: phpVoid,
GoFunction: `func simpleFunc() {
// simple function
}`,
},
},
classes: nil,
expectCgoImport: false,
description: "Extensions with only functions should not import runtime/cgo",
},
{
name: "extension with classes should include runtime/cgo import",
baseName: "with_classes",
sourceFile: createTempSourceFile(t, `package main

//export_php:class TestClass
type TestStruct struct {
name string
}

//export_php:method TestClass::getName(): string
func (ts *TestStruct) GetName() string {
return ts.name
}`),
functions: nil,
classes: []phpClass{
{
Name: "TestClass",
GoStruct: "TestStruct",
Methods: []phpClassMethod{
{
Name: "GetName",
PhpName: "getName",
ClassName: "TestClass",
Signature: "getName(): string",
ReturnType: phpString,
Params: []phpParameter{},
GoFunction: `func (ts *TestStruct) GetName() string {
return ts.name
}`,
},
},
},
},
expectCgoImport: true,
description: "Extensions with classes should import runtime/cgo for handle management",
},
{
name: "extension with functions and classes should include runtime/cgo import",
baseName: "mixed",
sourceFile: createTempSourceFile(t, `package main

//export_php: utilFunc(): string
func utilFunc() string {
return "utility"
}

//export_php:class MixedClass
type MixedStruct struct {
value int
}

//export_php:method MixedClass::getValue(): int
func (ms *MixedStruct) GetValue() int {
return ms.value
}`),
functions: []phpFunction{
{
Name: "utilFunc",
ReturnType: phpString,
GoFunction: `func utilFunc() string {
return "utility"
}`,
},
},
classes: []phpClass{
{
Name: "MixedClass",
GoStruct: "MixedStruct",
Methods: []phpClassMethod{
{
Name: "GetValue",
PhpName: "getValue",
ClassName: "MixedClass",
Signature: "getValue(): int",
ReturnType: phpInt,
Params: []phpParameter{},
GoFunction: `func (ms *MixedStruct) GetValue() int {
return ms.value
}`,
},
},
},
},
expectCgoImport: true,
description: "Extensions with both functions and classes should import runtime/cgo",
},
{
name: "empty extension should not include runtime/cgo import",
baseName: "empty",
sourceFile: createTempSourceFile(t, `package main

// Empty extension for testing
`),
functions: nil,
classes: nil,
expectCgoImport: false,
description: "Empty extensions should not import runtime/cgo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := &Generator{
BaseName: tt.baseName,
SourceFile: tt.sourceFile,
Functions: tt.functions,
Classes: tt.classes,
}

goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)

cgoImportPresent := strings.Contains(content, `import "runtime/cgo"`)

if tt.expectCgoImport {
assert.True(t, cgoImportPresent, "Extension should import runtime/cgo: %s", tt.description)
// Verify that cgo functions are also present when import is included
assert.Contains(t, content, "cgo.NewHandle", "Should contain cgo.NewHandle usage when runtime/cgo is imported")
assert.Contains(t, content, "cgo.Handle", "Should contain cgo.Handle usage when runtime/cgo is imported")
} else {
assert.False(t, cgoImportPresent, "Extension should not import runtime/cgo: %s", tt.description)
// Verify that cgo functions are not present when import is not included
assert.NotContains(t, content, "cgo.NewHandle", "Should not contain cgo.NewHandle usage when runtime/cgo is not imported")
assert.NotContains(t, content, "cgo.Handle", "Should not contain cgo.Handle usage when runtime/cgo is not imported")
}

// Ensure exactly one C import is always present
cImportCount := strings.Count(content, `import "C"`)
assert.Equal(t, 1, cImportCount, "Should have exactly one C import")
})
}
}
Loading
Loading