Экспорт функций

Руководство по экспорту функций из вашего плагина для использования другими языковыми модулями в Plugify.

В экосистеме Plugify плагины на Go могут экспортировать функции, чтобы их могли вызывать другие плагины. Это руководство объясняет, как определять и экспортировать функции в Go, и содержит примеры.

Базовое сопоставление типов

В таблице показано, какими типами Go представлены типы Plugify:

Тип C++Тип GoПсевдоним PlugifyПоддержка ссылок
voidvoid (в Go не используется)void❌
boolboolbool✅
charint8char8✅
char16_tuint16char16✅
int8_tint8int8✅
int16_tint16int16✅
int32_tint32int32✅
int64_tint64int64✅
uint8_tuint8uint8✅
uint16_tuint16uint16✅
uint32_tuint32uint32✅
uint64_tuint64uint64✅
uintptr_tuintptrptr64✅
uintptr_tuintptrptr32✅
floatfloat32float✅
doublefloat64double✅
void*именованный тип функцииfunction❌
plg::stringstringstring✅
plg::anyanyany✅
plg::vector<bool>[]boolbool[]✅
plg::vector<char>[]int8char8[]✅
plg::vector<char16_t>[]uint16char16[]✅
plg::vector<int8_t>[]int8int8[]✅
plg::vector<int16_t>[]int16int16[]✅
plg::vector<int32_t>[]int32int32[]✅
plg::vector<int64_t>[]int64int64[]✅
plg::vector<uint8_t>[]uint8uint8[]✅
plg::vector<uint16_t>[]uint16uint16[]✅
plg::vector<uint32_t>[]uint32uint32[]✅
plg::vector<uint64_t>[]uint64uint64[]✅
plg::vector<uintptr_t>[]uintptrptr64[]✅
plg::vector<uintptr_t>[]uintptrptr32[]✅
plg::vector<float>[]float32float[]✅
plg::vector<double>[]float64double[]✅
plg::vector<plg::string>[]stringstring[]✅
plg::vector<plg::any>[]anyany[]✅
plg::vector<plg::vec2>[]plugify.Vector2vec2[]✅
plg::vector<plg::vec3>[]plugify.Vector3vec3[]✅
plg::vector<plg::vec4>[]plugify.Vector4vec4[]✅
plg::vector<plg::mat4x4>[]plugify.Matrix4x4mat4x4[]✅
plg::vec2plugify.Vector2vec2✅
plg::vec3plugify.Vector3vec3✅
plg::vec4plugify.Vector4vec4✅
plg::mat4x4plugify.Matrix4x4mat4x4✅

Экспорт функций в Go

Чтобы экспортировать функцию, пометьте её комментарием //plugify:export Name. Функция должна быть экспортируемой в смысле Go (начинаться с заглавной буквы). Генератор go-plugify создаёт для неё cgo-обёртку __Name и описывает функцию в манифесте плагина, где funcName равен имени этой обёртки.

Использование генератора для упрощения экспорта функций

Генератор из go-plugify (его вызывает ваш generator.go, см. ниже) упрощает экспорт функций Go:

  1. Сканирует пакет плагина: находит функции с комментарием //plugify:export.
  2. Создаёт манифест: записывает манифест .pplugin с сигнатурами этих функций.
  3. Генерирует файлы: записывает autoexports.go и autoexports.h с cgo-обёртками, которые преобразуют типы Plugify в типы Go.

Маршалинг вручную писать не нужно.

Простой пример

Простой пример экспорта функции в плагине на Go:

Определение функции

exports.go
package main

// AddNumbers adds two integers.
//
//plugify:export AddNumbers
func AddNumbers(a int32, b int32) int32 {
    return a + b
}

Манифест плагина

Генератор описывает функцию в разделе methods манифеста:

go_example_plugin.pplugin
{
  "name": "go_example_plugin",
  "version": "0.1.0",
  "methods": [
    {
      "name": "AddNumbers",
      "funcName": "__AddNumbers",
      "paramTypes": [
        {
          "type": "int32",
          "name": "a"
        },
        {
          "type": "int32",
          "name": "b"
        }
      ],
      "retType": {
        "type": "int32"
      }
    }
  ]
}

Сгенерированный код

Сгенерированный autoexports.go содержит обёртку, которую вызывают другие плагины:

autoexports.go
package main

//#include "autoexports.h"
import "C"

//export __AddNumbers
func __AddNumbers(a int32, b int32) int32 {
    __result := AddNumbers(a, b)
    return __result
}

Этот код преобразует типы Plugify в типы Go и позволяет вызывать функцию из других плагинов.

Сложный пример: Экспорт сложных функций

Пример экспорта функции со сложными типами параметров и возвращаемого значения:

Определение функции

exports.go
package main

import "fmt"

// ProcessData formats each value with a prefix and joins the results.
//
//plugify:export ProcessData
func ProcessData(data []float64, prefix string) string {
    result := ""
    for _, value := range data {
        result += fmt.Sprintf("%s%f", prefix, value)
    }
    return result
}

Манифест плагина

go_example_plugin.pplugin
{
  "name": "go_example_plugin",
  "version": "0.1.0",
  "methods": [
    {
      "name": "ProcessData",
      "funcName": "__ProcessData",
      "paramTypes": [
        {
          "type": "double[]",
          "name": "data"
        },
        {
          "type": "string",
          "name": "prefix"
        }
      ],
      "retType": {
        "type": "string"
      }
    }
  ]
}

Сгенерированный код

Строки и срезы приходят как контейнеры Plugify; обёртка преобразует их в значения Go, а результат обратно:

autoexports.go
//export __ProcessData
func __ProcessData(data *C.Vector, prefix *C.String) C.String {
    __result := ProcessData(plugify.GetVectorDataDouble[float64]((*plugify.PlgVector)(unsafe.Pointer(data))), plugify.GetStringData[string]((*plugify.PlgString)(unsafe.Pointer(prefix))))
    __return := plugify.ConstructString(__result)
    return *(*C.String)(unsafe.Pointer(&__return))
}

Этот код преобразует типы Plugify в типы Go и позволяет вызывать функцию из других плагинов.

Экспорт функций с параметрами по ссылке

Plugify поддерживает параметры по ссылке (выходные или входно-выходные), через которые функция может изменить значения и вернуть их вызывающему. В Go такие параметры передаются указателями.

Определение функции с параметрами по ссылке

exports.go
package main

// IncrementValue increments an integer value by reference.
//
//plugify:export IncrementValue
func IncrementValue(value *int32) {
    *value += 1
}

// Calculate returns the sum and the product of two numbers via pointers.
//
//plugify:export Calculate
func Calculate(a int32, b int32, sum *int32, product *int32) {
    *sum = a + b
    *product = a * b
}

Манифест плагина с параметрами по ссылке

В манифесте параметры по ссылке помечены "ref": true:

go_example_plugin.pplugin
{
  "name": "go_example_plugin",
  "version": "0.1.0",
  "methods": [
    {
      "name": "IncrementValue",
      "funcName": "__IncrementValue",
      "paramTypes": [
        {
          "type": "int32",
          "name": "value",
          "ref": true
        }
      ],
      "retType": {
        "type": "void"
      }
    },
    {
      "name": "Calculate",
      "funcName": "__Calculate",
      "paramTypes": [
        {
          "type": "int32",
          "name": "a"
        },
        {
          "type": "int32",
          "name": "b"
        },
        {
          "type": "int32",
          "name": "sum",
          "ref": true
        },
        {
          "type": "int32",
          "name": "product",
          "ref": true
        }
      ],
      "retType": {
        "type": "void"
      }
    }
  ]
}

Сгенерированный код для параметров по ссылке

Генератор создаёт обёртки, которые передают указатели:

autoexports.go
//export __IncrementValue
func __IncrementValue(value *int32) {
    IncrementValue(value)
}

//export __Calculate
func __Calculate(a int32, b int32, sum *int32, product *int32) {
    Calculate(a, b, sum, product)
}

Поддержка параметров по ссылке

Параметры по ссылке работают с большинством типов Plugify, как показано в столбце «Поддержка ссылок» таблицы сопоставления типов. Следующие типы не поддерживают ссылки:

  • void (не может передаваться по ссылке)
  • function (типы колбэков)

Все остальные типы, включая примитивы, строки, срезы и структуры, передаются по ссылке через указатели.

Обработка обратных вызовов (Callbacks)

Plugify позволяет экспортировать функции, принимающие колбэки. Тип колбэка должен быть именованным типом функции Go:

Определение функции

exports.go
package main

import "fmt"

// ExampleCallback is the callback signature.
type ExampleCallback func(value int32, inputStr string) string

// ExecuteWithCallback calls the callback and prints its result.
//
//plugify:export ExecuteWithCallback
func ExecuteWithCallback(value int32, inputStr string, callback ExampleCallback) {
    result := callback(value, inputStr)
    fmt.Printf("Callback result: %s\n", result)
}

Манифест плагина

Генератор выносит сигнатуру колбэка в prototypes и ссылается на неё по имени:

go_example_plugin.pplugin
{
  "name": "go_example_plugin",
  "version": "0.1.0",
  "methods": [
    {
      "name": "ExecuteWithCallback",
      "funcName": "__ExecuteWithCallback",
      "paramTypes": [
        {
          "type": "int32",
          "name": "value"
        },
        {
          "type": "string",
          "name": "inputStr"
        },
        {
          "type": "function",
          "name": "callback",
          "prototype": "ExampleCallback"
        }
      ],
      "retType": {
        "type": "void"
      }
    }
  ],
  "prototypes": [
    {
      "name": "ExampleCallback",
      "paramTypes": [
        {
          "type": "int32",
          "name": "value"
        },
        {
          "type": "string",
          "name": "inputStr"
        }
      ],
      "retType": {
        "type": "string"
      }
    }
  ]
}

Сгенерированный код

Обёртка превращает нативный указатель на функцию в значение вашего типа функции Go:

autoexports.go
//export __ExecuteWithCallback
func __ExecuteWithCallback(value int32, inputStr *C.String, callback unsafe.Pointer) {
    ExecuteWithCallback(value, plugify.GetStringData[string]((*plugify.PlgString)(unsafe.Pointer(inputStr))), plugify.GetDelegateForFunctionPointer(callback, reflect.TypeOf(ExampleCallback(nil))).(ExampleCallback))
}

Автоматизация генерации манифеста и экспорта

Манифест и код экспорта не пишутся вручную: вы помечаете функции комментариями //plugify:export Name и запускаете go generate. Генератор разбирает пакет и записывает:

  1. Манифест плагина (файл .pplugin) со всеми экспортируемыми методами
  2. Файлы экспорта (autoexports.go и autoexports.h) с кодом маршалинга

Преимущества автоматической генерации

  1. Без ручного JSON: сигнатуры функций берутся из кода
  2. Полная информация о типах: типы параметров и возвращаемых значений сопоставляются автоматически
  3. Автоматический маршалинг: генерируются обёртки с преобразованием типов
  4. Меньше ошибок: код и манифест не расходятся
  5. Простая поддержка: изменения сигнатур попадают в манифест при следующей генерации

Настройка: Создание вашего генератора

Генератор входит в пакет go-plugify, но запускается из вашего собственного generator.go, который вызывает plugify.Generate(). В шаблоне Go-плагина этот файл уже есть:

generator.go
//go:build ignore

package main

import (
    "flag"
    "fmt"
    "os"
    "strings"

    "github.com/untrustedmodders/go-plugify"
)

func main() {
    var (
        patterns     = flag.String("patterns", "*", "Package patterns to analyze")
        output       = flag.String("output", "", "Output manifest file (default: <packagename>.pplugin)")
        name         = flag.String("name", "", "Plugin name (default: package name)")
        version      = flag.String("version", "1.0.0", "Plugin version")
        description  = flag.String("description", "", "Plugin description")
        author       = flag.String("author", "", "Plugin author")
        website      = flag.String("website", "", "Plugin website")
        license      = flag.String("license", "", "Plugin license")
        platforms    = flag.String("platforms", "", "Comma-separated list of platforms (e.g., windows,linux,darwin)")
        dependencies = flag.String("dependencies", "", "Comma-separated list of dependencies (e.g., plugin1,plugin2)")
        conflicts    = flag.String("conflicts", "", "Comma-separated list of conflicts (e.g., plugin3,plugin4)")
        entry        = flag.String("entry", "", "Plugin entry point (default: <packagename>)")
    )

    flag.Parse()

    err := plugify.Generate(*output, *name, *entry, true, plugify.GenerateParams{
        Version:      *version,
        Description:  *description,
        Author:       *author,
        Website:      *website,
        License:      *license,
        Platforms:    parseCommaSeparated(*platforms),
        Dependencies: parseCommaSeparated(*dependencies),
        Conflicts:    parseCommaSeparated(*conflicts),
    }, &plugify.LoadPackageFilter{
        PathPatterns: []string{*patterns},
    })
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error generating plugin manifest: %v\n", err)
        os.Exit(1)
    }
}

// parseCommaSeparated parses a comma-separated string into a slice of trimmed strings
func parseCommaSeparated(input string) []string {
    if input == "" {
        return nil
    }

    parts := strings.Split(input, ",")
    result := make([]string, 0, len(parts))

    for _, part := range parts {
        part = strings.TrimSpace(part)
        if part != "" {
            result = append(result, part)
        }
    }

    return result
}

Строка //go:build ignore исключает generator.go из сборки плагина. Директива //go:generate, которая его запускает, пишется в одном из исходных файлов плагина, например в main.go:

main.go
//go:generate go run generator.go -output go_example_plugin.pplugin -name go_example_plugin -version 0.1.0 -entry bin/go_example_plugin -author untrustedmodders -website https://github.com/untrustedmodders/ -license MIT -description "An example of a Go plugin."

package main

description, author, website и license являются обязательными полями манифеста: без этих флагов Plugify отклонит сгенерированный манифест.

Использование комментария //plugify:export

Пометьте экспортируемые функции комментарием //plugify:export FuncName:

exports.go
package main

import "fmt"

// AddNumbers adds two integers.
//
//plugify:export AddNumbers
func AddNumbers(a int32, b int32) int32 {
    return a + b
}

// ProcessDataList formats each value with a prefix.
//
//plugify:export ProcessDataList
func ProcessDataList(data []float64, prefix string) []string {
    var result []string
    for _, value := range data {
        result = append(result, fmt.Sprintf("%s%f", prefix, value))
    }
    return result
}

Запуск генератора

Запустите генератор командой go generate:

go generate

Или вручную с теми же флагами:

go run generator.go -output my_plugin.pplugin -name my_plugin -version 1.0.0 -entry bin/my_plugin -author "Your Name" -website https://example.com -license MIT -description "My plugin"

Затем соберите плагин как обычно (go build -buildmode=c-shared).

Как это работает

  1. Генерация: парсер Go ищет в пакете комментарии //plugify:export
  2. Анализ типов: из сигнатур функций типы Go сопоставляются с типами Plugify (например, int32 → int32, []string → string[])
  3. Манифест: создаётся файл .pplugin со всеми экспортируемыми методами
  4. Код экспорта: создаются autoexports.go и autoexports.h с кодом маршалинга

Лучшие практики

  1. Используйте //plugify:export: помечайте функции для генерации комментарием //plugify:export FuncName.
  2. Заведите файл генератора: свой generator.go, вызывающий plugify.Generate().
  3. Следуйте соглашениям о типах: используйте типы из таблицы сопоставления.
  4. Документируйте функции: комментариями Go к экспортируемым функциям.
  5. Используйте go generate: перегенерируйте манифест и файлы экспорта после изменений.
  6. Тестируйте: проверяйте, что экспортируемые функции работают при вызове из других плагинов.
  7. Держите сгенерированные файлы актуальными: перегенерируйте после любого изменения сигнатур.

Заключение

Генератор из go-plugify упрощает экспорт функций в плагинах на Go. Пометив функции комментариями //plugify:export и запустив go generate, вы получаете и манифест плагина, и код экспорта с маршалингом типов. JSON не нужно писать вручную, ошибок меньше. Сложный маршалинг колбэков генератор тоже берёт на себя.