blob: 9a129adcc5ba14f5531b1d96fcec933eb541e5ce [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"flag"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"sort"
"strings"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
)
func main() {
inputCPPDir := flag.String("input_cpp_dir", "", "A folder containing .cpp binding files that make use of the TS_* macros")
outputNamespaceDir := flag.String("output_namespace_dir", "", "The folder where the ambient namespace (.d.ts) files that correspond to the C++ API should be written")
flag.Parse()
if *inputCPPDir == "" || *outputNamespaceDir == "" {
sklog.Fatalf("--input_cpp_dir and --output_namespace_dir must be specified")
}
cppInputs, err := ioutil.ReadDir(*inputCPPDir)
if err != nil {
sklog.Fatalf("Could not read directory %s: %s", *inputCPPDir, err)
}
for _, file := range cppInputs {
name := file.Name()
if strings.HasSuffix(name, ".cpp") {
fp := filepath.Join(*inputCPPDir, name)
contents, err := ioutil.ReadFile(fp)
if err != nil {
sklog.Fatalf("Could not read file %s: %s", fp, err)
}
namespace := strings.TrimSuffix(name, ".cpp")
output, err := generateAmbientNamespace(namespace, string(contents))
if err != nil {
sklog.Fatalf("Could not generate ambient namespace from %s: %s", fp, err)
}
fp = filepath.Join(*outputNamespaceDir, namespace+".d.ts")
if err := ioutil.WriteFile(fp, []byte(output), 0666); err != nil {
sklog.Fatalf("Could not write to %s: %s", fp, err)
}
}
}
}
var (
privateExportLine = regexp.MustCompile(`TS_PRIVATE_EXPORT\("(?P<export>.+)"\)`)
publicExportLine = regexp.MustCompile(`TS_EXPORT\("(?P<export>.+)"\)`)
classDefinitionStart = regexp.MustCompile(`class_<.+>\("(?P<name>.+)"\)`)
classDefinitionEnd = regexp.MustCompile(`\.(function|constructor).+;`)
typeAnnotation = regexp.MustCompile(`@type\s+(?P<optional>@optional)?\s*(?P<type>\w+)`)
valueObjectStart = regexp.MustCompile(`value_object<.+>\("(?P<name>.+)"\)`)
valueObjectField = regexp.MustCompile(`.field\("(?P<name>.+?)",.+\);?`)
constantDefinition = regexp.MustCompile(`constant\("(?P<name>.+?)",.+\);`)
)
// generateAmbientNamespace reads through the given file contents line by line. It looks for
// explicit macro annotations (e.g. TS_EXPORT) and embind signatures (e.g. constant("foo", "bar").
// From these, it generates the ambient namespace declarations and returns it as a string.
// In general, it will put declarations in alphabetical order for deterministic ordering.
func generateAmbientNamespace(namespace, contents string) (string, error) {
var privateModuleFunctions []string
var publicModuleFunctions []string
var wasmObjects []*wasmObject
var valueObjects []*valueObject
var constants []string
var currentWasmObject *wasmObject
var currentValueObject *valueObject
var lastTypeAnnotation string
var lastTypeOptional bool
lines := strings.Split(contents, "\n")
for i, line := range lines {
if match := classDefinitionStart.FindStringSubmatch(line); match != nil {
currentWasmObject = &wasmObject{
name: match[1],
}
wasmObjects = append(wasmObjects, currentWasmObject)
}
if classDefinitionEnd.MatchString(line) {
currentWasmObject = nil
}
if match := privateExportLine.FindStringSubmatch(line); match != nil {
export := match[1]
if currentWasmObject == nil {
privateModuleFunctions = append(privateModuleFunctions, export)
} else {
currentWasmObject.privateMethods = append(currentWasmObject.privateMethods, export)
}
}
if match := publicExportLine.FindStringSubmatch(line); match != nil {
export := match[1]
if currentWasmObject == nil {
publicModuleFunctions = append(publicModuleFunctions, export)
} else {
if strings.HasPrefix(export, "new") {
currentWasmObject.constructors = append(currentWasmObject.constructors, export)
} else {
currentWasmObject.publicMethods = append(currentWasmObject.publicMethods, export)
}
}
}
if match := typeAnnotation.FindStringSubmatch(line); match != nil {
lastTypeOptional = match[1] != ""
lastTypeAnnotation = match[2]
}
if match := valueObjectStart.FindStringSubmatch(line); match != nil {
currentValueObject = &valueObject{
name: match[1],
}
valueObjects = append(valueObjects, currentValueObject)
}
if match := valueObjectField.FindStringSubmatch(line); match != nil {
if currentValueObject == nil {
// Should never happen for valid code - embind wouldn't compile with this.
return "", skerr.Fmt("Line %d: Found a .field outside a value object declaration", i+1)
}
if lastTypeAnnotation == "" {
return "", skerr.Fmt("Line %d: field %q must be preceded by a @type annotation", i+1, match[1])
}
fieldWithType := fmt.Sprintf("%s: %s", match[1], lastTypeAnnotation)
if lastTypeOptional {
fieldWithType = fmt.Sprintf("%s?: %s", match[1], lastTypeAnnotation)
}
currentValueObject.fields = append(currentValueObject.fields, fieldWithType)
lastTypeAnnotation = "" // It's been consumed, so reset it
lastTypeOptional = false
if strings.HasSuffix(match[0], ";") {
currentValueObject = nil // we've gotten to the end of our value object
}
}
if match := constantDefinition.FindStringSubmatch(line); match != nil {
if lastTypeAnnotation == "" {
return "", skerr.Fmt("Line %d: constant %q must be preceded by a @type annotation", i+1, match[1])
}
constWithType := fmt.Sprintf("readonly %s: %s", match[1], lastTypeAnnotation)
if lastTypeOptional {
constWithType = fmt.Sprintf("readonly %s?: %s", match[1], lastTypeAnnotation)
}
constants = append(constants, constWithType)
lastTypeAnnotation = "" // It's been consumed, so reset it
lastTypeOptional = false
}
}
sort.Strings(privateModuleFunctions)
sort.Strings(publicModuleFunctions)
sort.Slice(wasmObjects, func(i, j int) bool {
return wasmObjects[i].name < wasmObjects[j].name
})
sort.Slice(valueObjects, func(i, j int) bool {
return valueObjects[i].name < valueObjects[j].name
})
sort.Strings(constants)
output := fmt.Sprintf(`/// <reference path="embind.d.ts" />
/* This file is autogenerated using gen_types.go and make generate */
declare namespace %s {
export interface Bindings {
`, namespace)
for _, export := range privateModuleFunctions {
output += fmt.Sprintf("\t\t%s\n", ensureSemicolon(export))
}
output += "\n"
for _, export := range publicModuleFunctions {
output += fmt.Sprintf("\t\t%s\n", ensureSemicolon(export))
}
output += "\n"
for _, obj := range wasmObjects {
output += fmt.Sprintf("\t\treadonly %s: %sConstructor;\n", obj.name, obj.name)
}
output += "\n"
for _, c := range constants {
output += fmt.Sprintf("\t\t%s;\n", c)
}
output += "\t}\n\n"
// The constructors for all exposed objects.
for _, obj := range wasmObjects {
sort.Strings(obj.constructors)
output += fmt.Sprintf("\texport interface %sConstructor {\n", obj.name)
for _, c := range obj.constructors {
output += fmt.Sprintf("\t\t%s\n", ensureSemicolon(c))
}
output += "\t}\n\n"
}
// The exposed objects themselves
for _, obj := range wasmObjects {
sort.Strings(obj.privateMethods)
sort.Strings(obj.publicMethods)
output += fmt.Sprintf("\texport interface %s extends embind.EmbindObject<%s> {\n", obj.name, obj.name)
for _, m := range obj.privateMethods {
output += fmt.Sprintf("\t\t%s\n", ensureSemicolon(m))
}
if len(obj.privateMethods) > 0 {
output += "\n"
}
for _, m := range obj.publicMethods {
output += fmt.Sprintf("\t\t%s\n", ensureSemicolon(m))
}
output += "\t}\n\n"
}
for _, obj := range valueObjects {
sort.Strings(obj.fields)
output += fmt.Sprintf("\texport interface %s {\n", obj.name)
for _, f := range obj.fields {
output += fmt.Sprintf("\t\t%s,\n", f)
}
output += "\t}\n\n"
}
output = strings.TrimSuffix(output, "\n")
output += "}\n"
return output, nil
}
func ensureSemicolon(js string) string {
if !strings.HasSuffix(js, ";") {
return js + ";"
}
return js
}
type wasmObject struct {
name string
constructors []string
publicMethods []string
privateMethods []string
}
type valueObject struct {
name string
fields []string
}