Compare commits

...

14 Commits

Author SHA1 Message Date
Robert Landers
12311107f4 error when there is more than one module:init or module:shutdown
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:57:23 +02:00
Robert Landers
c57e1c6d2a combine var
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:52:24 +02:00
Robert Landers
44d58e3590 use literals
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:52:11 +02:00
Robert Landers
36cdb72536 fix newlines
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:34:32 +02:00
Robert Landers
17eba05bcd remove EXPERIMENTAL
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:33:34 +02:00
Robert Landers
50208aa818 fix the regex
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:33:20 +02:00
Robert Landers
f76fd14c3f only import runtime/cgo when it needs to
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:42 +02:00
Robert Landers
6c208e2753 Revert "remove import that causes issues"
This reverts commit 3d9d672248778852ffa47c9bad238b2587832077.
2025-08-11 19:06:41 +02:00
Robert Landers
31e045bb75 remove import that causes issues
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:40 +02:00
Robert Landers
0d9dda91e9 handle file.close error
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:39 +02:00
Robert Landers
74e9e9aa19 handle init function case
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:38 +02:00
Robert Landers
327a20ce63 update to handle tagging specific functions
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:37 +02:00
Robert Landers
8efbc6c1e2 add tests
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:36 +02:00
Robert Landers
7ea6e7c093 add ability to specify startup/shutdown functions
Signed-off-by: Robert Landers <landers.robert@gmail.com>
2025-08-11 19:06:35 +02:00
10 changed files with 720 additions and 8 deletions

View File

@@ -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.

View File

@@ -23,6 +23,7 @@ type cTemplateData struct {
Classes []phpClass
Constants []phpConstant
Namespace string
Module *phpModule
}
func (cg *cFileGenerator) generate() error {
@@ -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
}

View File

@@ -15,6 +15,7 @@ type Generator struct {
Classes []phpClass
Constants []phpConstant
Namespace string
Module *phpModule
}
// EXPERIMENTAL
@@ -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
}

View File

@@ -5,6 +5,7 @@ import (
_ "embed"
"fmt"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
@@ -25,6 +26,8 @@ type goTemplateData struct {
InternalFunctions []string
Functions []phpFunction
Classes []phpClass
Module *phpModule
HasInitFunction bool
}
func (gg *GoFileGenerator) generate() error {
@@ -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,
@@ -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 {

View File

@@ -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{
@@ -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,
@@ -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,
},
}
@@ -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")
})
}
}

View File

@@ -0,0 +1,137 @@
package extgen
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
var (
phpModuleParser = regexp.MustCompile(`//\s*export_php:module\s+(init|shutdown)`)
funcNameRegex = regexp.MustCompile(`func\s+([a-zA-Z0-9_]+)`)
)
// phpModule represents a PHP module with optional init and shutdown functions
type phpModule struct {
InitFunc string // Name of the init function
InitCode string // Code of the init function
ShutdownFunc string // Name of the shutdown function
ShutdownCode string // Code of the shutdown function
}
// ModuleParser parses PHP module directives from Go source files
type ModuleParser struct{}
// parse parses the source file for PHP module directives
func (mp *ModuleParser) parse(filename string) (module *phpModule, err error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer func() {
e := file.Close()
if err == nil {
err = e
}
}()
scanner := bufio.NewScanner(file)
module = &phpModule{}
var (
currentDirective string
lineNumber int
)
for scanner.Scan() {
lineNumber++
line := strings.TrimSpace(scanner.Text())
if matches := phpModuleParser.FindStringSubmatch(line); matches != nil {
directiveType := matches[1]
currentDirective = directiveType
continue
}
// If we have a current directive and encounter a non-comment line
// that doesn't start with "func ", reset the current directive
if currentDirective != "" && (line == "" || (!strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "func "))) {
currentDirective = ""
continue
}
if currentDirective != "" && strings.HasPrefix(line, "func ") {
funcName, funcCode, err := mp.extractGoFunction(scanner, line)
if err != nil {
return nil, fmt.Errorf("extracting Go function at line %d: %w", lineNumber, err)
}
switch currentDirective {
case "init":
if module.InitFunc != "" {
return nil, fmt.Errorf("duplicate init directive found at line %d: init function '%s' already defined", lineNumber, module.InitFunc)
}
module.InitFunc = funcName
module.InitCode = funcCode
case "shutdown":
if module.ShutdownFunc != "" {
return nil, fmt.Errorf("duplicate shutdown directive found at line %d: shutdown function '%s' already defined", lineNumber, module.ShutdownFunc)
}
module.ShutdownFunc = funcName
module.ShutdownCode = funcCode
}
currentDirective = ""
}
}
// If we found no module functions, return nil
if module.InitFunc == "" && module.ShutdownFunc == "" {
return nil, nil
}
return module, scanner.Err()
}
// extractGoFunction extracts the function name and code from a function declaration
func (mp *ModuleParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, string, error) {
// Extract function name from the first line
matches := funcNameRegex.FindStringSubmatch(firstLine)
if len(matches) < 2 {
return "", "", fmt.Errorf("could not extract function name from line: %s", firstLine)
}
funcName := matches[1]
// Collect the function code
goFunc := firstLine + "\n"
braceCount := 0
// Count opening braces in the first line
for _, char := range firstLine {
if char == '{' {
braceCount++
}
}
// Continue reading until we find the matching closing brace
for braceCount > 0 && scanner.Scan() {
line := scanner.Text()
goFunc += line + "\n"
for _, char := range line {
switch char {
case '{':
braceCount++
case '}':
braceCount--
}
}
if braceCount == 0 {
break
}
}
return funcName, goFunc, nil
}

View File

@@ -0,0 +1,321 @@
package extgen
import (
"bufio"
"github.com/stretchr/testify/require"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestModuleParser(t *testing.T) {
tests := []struct {
name string
input string
expected *phpModule
}{
{
name: "both init and shutdown",
input: `package main
//export_php:module init
func initializeModule() {
// Initialization code
}
//export_php:module shutdown
func cleanupModule() {
// Cleanup code
}`,
expected: &phpModule{
InitFunc: "initializeModule",
ShutdownFunc: "cleanupModule",
},
},
{
name: "only init function",
input: `package main
//export_php:module init
func initializeModule() {
// Initialization code
}`,
expected: &phpModule{
InitFunc: "initializeModule",
ShutdownFunc: "",
},
},
{
name: "only shutdown function",
input: `package main
//export_php:module shutdown
func cleanupModule() {
// Cleanup code
}`,
expected: &phpModule{
InitFunc: "",
ShutdownFunc: "cleanupModule",
},
},
{
name: "with extra whitespace",
input: `package main
//export_php:module init
func initModule() {
// Initialization code
}
//export_php:module shutdown
func shutdownModule() {
// Cleanup code
}`,
expected: &phpModule{
InitFunc: "initModule",
ShutdownFunc: "shutdownModule",
},
},
{
name: "no module directive",
input: `package main
func regularFunction() {
// Just a regular Go function
}`,
expected: nil,
},
{
name: "functions with braces",
input: `package main
//export_php:module init
func initModule() {
if true {
// Do something
}
for i := 0; i < 10; i++ {
// Loop
}
}
//export_php:module shutdown
func shutdownModule() {
if true {
// Do something else
}
}`,
expected: &phpModule{
InitFunc: "initModule",
ShutdownFunc: "shutdownModule",
},
},
{
name: "multiple functions between directives",
input: `package main
//export_php:module init
func initModule() {
// Init code
}
func someOtherFunction() {
// This should be ignored
}
//export_php:module shutdown
func shutdownModule() {
// Shutdown code
}`,
expected: &phpModule{
InitFunc: "initModule",
ShutdownFunc: "shutdownModule",
},
},
{
name: "directive without function",
input: `package main
//export_php:module init
// No function follows
func regularFunction() {
// This should be ignored
}`,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, tt.name+".go")
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
parser := &ModuleParser{}
module, err := parser.parse(fileName)
require.NoError(t, err)
if tt.expected == nil {
assert.Nil(t, module, "parse() should return nil for no module directive")
} else {
assert.NotNil(t, module, "parse() should not return nil")
assert.Equal(t, tt.expected.InitFunc, module.InitFunc, "InitFunc mismatch")
assert.Equal(t, tt.expected.ShutdownFunc, module.ShutdownFunc, "ShutdownFunc mismatch")
// Check that function code was extracted
if tt.expected.InitFunc != "" {
assert.Contains(t, module.InitCode, "func "+tt.expected.InitFunc, "InitCode should contain function declaration")
assert.True(t, strings.HasSuffix(module.InitCode, "}\n"), "InitCode should end with closing brace")
}
if tt.expected.ShutdownFunc != "" {
assert.Contains(t, module.ShutdownCode, "func "+tt.expected.ShutdownFunc, "ShutdownCode should contain function declaration")
assert.True(t, strings.HasSuffix(module.ShutdownCode, "}\n"), "ShutdownCode should end with closing brace")
}
}
})
}
}
func TestExtractGoFunction(t *testing.T) {
tests := []struct {
name string
input string
firstLine string
expectedName string
expectedPrefix string
expectedSuffix string
}{
{
name: "simple function",
input: "func testFunc() {\n\t// Some code\n}\n",
firstLine: "func testFunc() {",
expectedName: "testFunc",
expectedPrefix: "func testFunc() {",
expectedSuffix: "}\n",
},
{
name: "function with parameters",
input: "func initModule(param1 string, param2 int) {\n\t// Init code\n}\n",
firstLine: "func initModule(param1 string, param2 int) {",
expectedName: "initModule",
expectedPrefix: "func initModule(param1 string, param2 int) {",
expectedSuffix: "}\n",
},
{
name: "function with nested braces",
input: "func complexFunc() {\n\tif true {\n\t\t// Nested code\n\t}\n}\n",
firstLine: "func complexFunc() {",
expectedName: "complexFunc",
expectedPrefix: "func complexFunc() {",
expectedSuffix: "}\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := &ModuleParser{}
scanner := bufio.NewScanner(strings.NewReader(tt.input))
scanner.Scan() // Read the first line
name, code, err := parser.extractGoFunction(scanner, tt.firstLine)
assert.NoError(t, err)
assert.Equal(t, tt.expectedName, name)
assert.True(t, strings.HasPrefix(code, tt.expectedPrefix), "Function code should start with the declaration")
assert.True(t, strings.HasSuffix(code, tt.expectedSuffix), "Function code should end with closing brace")
})
}
}
func TestModuleParserErrors(t *testing.T) {
tests := []struct {
name string
input string
expectedErr string
}{
{
name: "duplicate init directives",
input: `package main
//export_php:module init
func firstInit() {
// First init function
}
//export_php:module init
func secondInit() {
// Second init function - should error
}`,
expectedErr: "duplicate init directive",
},
{
name: "duplicate shutdown directives",
input: `package main
//export_php:module shutdown
func firstShutdown() {
// First shutdown function
}
//export_php:module shutdown
func secondShutdown() {
// Second shutdown function - should error
}`,
expectedErr: "duplicate shutdown directive",
},
{
name: "multiple duplicates",
input: `package main
//export_php:module init
func firstInit() {
// First init function
}
//export_php:module init
func secondInit() {
// Duplicate init - should error
}
//export_php:module shutdown
func firstShutdown() {
// First shutdown function
}
//export_php:module shutdown
func secondShutdown() {
// Duplicate shutdown - should error
}`,
expectedErr: "duplicate init directive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
fileName := filepath.Join(tmpDir, tt.name+".go")
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
parser := &ModuleParser{}
module, err := parser.parse(fileName)
assert.Error(t, err, "parse() should return error for duplicate directives")
assert.Contains(t, err.Error(), tt.expectedErr, "error message should contain expected text")
assert.Nil(t, module, "parse() should return nil when there's an error")
})
}
}
func TestModuleParserFileErrors(t *testing.T) {
parser := &ModuleParser{}
// Test with non-existent file
module, err := parser.parse("non_existent_file.go")
assert.Error(t, err, "parse() should return error for non-existent file")
assert.Nil(t, module, "parse() should return nil for non-existent file")
}

View File

@@ -2,26 +2,27 @@ package extgen
type SourceParser struct{}
// EXPERIMENTAL
func (p *SourceParser) ParseFunctions(filename string) ([]phpFunction, error) {
functionParser := &FuncParser{}
return functionParser.parse(filename)
}
// EXPERIMENTAL
func (p *SourceParser) ParseClasses(filename string) ([]phpClass, error) {
classParser := classParser{}
return classParser.parse(filename)
}
// EXPERIMENTAL
func (p *SourceParser) ParseConstants(filename string) ([]phpConstant, error) {
constantParser := &ConstantParser{}
return constantParser.parse(filename)
}
// EXPERIMENTAL
func (p *SourceParser) ParseNamespace(filename string) (string, error) {
namespaceParser := NamespaceParser{}
return namespaceParser.parse(filename)
}
func (p *SourceParser) ParseModule(filename string) (*phpModule, error) {
moduleParser := &ModuleParser{}
return moduleParser.parse(filename)
}

View File

@@ -167,14 +167,28 @@ PHP_MINIT_FUNCTION({{.BaseName}}) {
{{- end}}
{{- end}}
{{- end}}
{{if and .Module .Module.InitFunc}}
{{.Module.InitFunc}}();
{{end}}
return SUCCESS;
}
{{if .Module}}
{{if .Module.ShutdownFunc}}
PHP_MSHUTDOWN_FUNCTION({{.BaseName}}) {
{{.Module.ShutdownFunc}}();
return SUCCESS;
}
{{end}}
{{end}}
zend_module_entry {{.BaseName}}_module_entry = {STANDARD_MODULE_HEADER,
"{{.BaseName}}",
ext_functions, /* Functions */
PHP_MINIT({{.BaseName}}), /* MINIT */
NULL, /* MSHUTDOWN */
{{if and .Module .Module.ShutdownFunc}}PHP_MSHUTDOWN({{.BaseName}}),{{else}}NULL,{{end}} /* MSHUTDOWN */
NULL, /* RINIT */
NULL, /* RSHUTDOWN */
NULL, /* MINFO */

View File

@@ -5,14 +5,18 @@ package {{.PackageName}}
#include "{{.BaseName}}.h"
*/
import "C"
{{- if .Classes}}
import "runtime/cgo"
{{- end}}
{{- range .Imports}}
import {{.}}
{{- end}}
{{if not .HasInitFunction}}
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
{{end}}
{{range .Constants}}
const {{.Name}} = {{.Value}}
{{- end}}
@@ -20,6 +24,18 @@ const {{.Name}} = {{.Value}}
{{.}}
{{- end}}
{{- if .Module}}
{{- if .Module.InitFunc}}
//export {{.Module.InitFunc}}
{{.Module.InitCode}}
{{- end}}
{{- if .Module.ShutdownFunc}}
//export {{.Module.ShutdownFunc}}
{{.Module.ShutdownCode}}
{{- end}}
{{- end}}
{{- range .Functions}}
//export {{.Name}}
{{.GoFunction}}