1
0
mirror of https://github.com/godotengine/godot.git synced 2025-12-02 16:48:55 +00:00

GDExtension: Store source of gdextension_interface.h in JSON

This commit is contained in:
David Snopek
2025-06-19 20:30:23 -05:00
parent 2cc031f3a3
commit 2c681794cd
16 changed files with 10371 additions and 3259 deletions

View File

@@ -41,7 +41,3 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install libxml2-utils sudo apt-get install libxml2-utils
xmllint --quiet --noout --schema doc/class.xsd doc/classes/*.xml modules/*/doc_classes/*.xml platform/*/doc_classes/*.xml xmllint --quiet --noout --schema doc/class.xsd doc/classes/*.xml modules/*/doc_classes/*.xml platform/*/doc_classes/*.xml
- name: Run C compiler on `gdextension_interface.h`
run: |
gcc -c core/extension/gdextension_interface.h

View File

@@ -57,6 +57,13 @@ repos:
- id: codespell - id: codespell
additional_dependencies: [tomli] additional_dependencies: [tomli]
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.34.1
hooks:
- id: check-jsonschema
files: ^core/extension/gdextension_interface\.json$
args: ["--schemafile", "core/extension/gdextension_interface.schema.json"]
### Requires Docker; look into alternative implementation. ### Requires Docker; look into alternative implementation.
# - repo: https://github.com/comkieffer/pre-commit-xmllint.git # - repo: https://github.com/comkieffer/pre-commit-xmllint.git
# rev: 1.0.0 # rev: 1.0.0

View File

@@ -4,14 +4,20 @@ from misc.utility.scons_hints import *
Import("env") Import("env")
import make_interface_dumper import make_interface_dumper
import make_interface_header
import make_wrappers import make_wrappers
env.CommandNoCache(["ext_wrappers.gen.inc"], "make_wrappers.py", env.Run(make_wrappers.run)) env.CommandNoCache(["ext_wrappers.gen.inc"], "make_wrappers.py", env.Run(make_wrappers.run))
env.CommandNoCache( env.CommandNoCache(
"gdextension_interface_dump.gen.h", "gdextension_interface_dump.gen.h",
["gdextension_interface.h", "make_interface_dumper.py"], ["gdextension_interface.json", "make_interface_dumper.py"],
env.Run(make_interface_dumper.run), env.Run(make_interface_dumper.run),
) )
env.CommandNoCache(
"gdextension_interface.gen.h",
["gdextension_interface.json", "make_interface_header.py"],
env.Run(make_interface_header.run),
)
env_extension = env.Clone() env_extension = env.Clone()

View File

@@ -30,7 +30,7 @@
#pragma once #pragma once
#include "core/extension/gdextension_interface.h" #include "core/extension/gdextension_interface.gen.h"
#include "core/extension/gdextension_loader.h" #include "core/extension/gdextension_loader.h"
#include "core/io/config_file.h" #include "core/io/config_file.h"
#include "core/io/resource_loader.h" #include "core/io/resource_loader.h"

View File

@@ -28,7 +28,7 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/ /**************************************************************************/
#include "gdextension_interface.h" #include "gdextension_interface.gen.h"
#include "core/config/engine.h" #include "core/config/engine.h"
#include "core/extension/gdextension.h" #include "core/extension/gdextension.h"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,290 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://godotengine.org/gdextension_interface.schema.json",
"title": "GDExtension Interface",
"type": "object",
"properties": {
"_copyright": {
"type": "array",
"items": {
"type": "string"
}
},
"format_version": {
"type": "number",
"const": 1
},
"$schema": {
"type": "string",
"const": "./gdextension_interface.schema.json"
},
"types": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"kind": {
"type": "string",
"enum": ["enum", "handle", "alias", "struct", "function"]
},
"description": {
"type": "array",
"items": {
"type": "string"
}
},
"deprecated": {
"type": "string"
}
},
"required": [
"name",
"kind"
],
"unevaluatedProperties": false,
"allOf": [
{
"if": {
"properties": {
"kind": { "const": "enum" }
}
},
"then": {
"properties": {
"values": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [ "name", "value" ],
"additionalProperties": false
}
}
},
"required": ["name", "kind", "values"]
}
},
{
"if": {
"properties": {
"kind": { "const": "handle" }
}
},
"then": {
"properties": {
"parent": {
"type": "string"
},
"const": {
"type": "boolean"
}
}
}
},
{
"if": {
"properties": {
"kind": { "const": "alias" }
}
},
"then": {
"properties": {
"type": {
"type": "string"
}
},
"required": ["name", "kind", "type"]
}
},
{
"if": {
"properties": {
"kind": { "const": "struct" }
}
},
"then": {
"properties": {
"members": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["name", "type"],
"additionalProperties": false
}
}
},
"required": ["name", "kind", "members"]
}
},
{
"if": {
"properties": {
"kind": { "const": "function" }
}
},
"then": {
"properties": {
"return_value": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["type"],
"additionalProperties": false
},
"arguments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["type"],
"additionalProperties": false
}
}
},
"required": ["name", "kind", "return_value", "arguments"]
}
}
]
}
},
"interface": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"return_value": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["type"],
"additionalProperties": false
},
"arguments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["type"],
"additionalProperties": false
}
},
"description": {
"type": "array",
"items": {
"type": "string"
}
},
"since": {
"type": "string",
"pattern": "4\\.[0-9]+"
},
"deprecated": {
"type": "string"
},
"see": {
"type": "array",
"items": {
"type": "string"
}
},
"legacy_type_name": {
"type": "string"
}
},
"required": [
"name",
"return_value",
"arguments",
"description",
"since"
],
"additionalProperties": false
}
}
},
"required": [
"_copyright",
"$schema",
"format_version",
"types",
"interface"
],
"additionalProperties": false
}

View File

@@ -0,0 +1,309 @@
/**************************************************************************/
/* gdextension_interface_header_generator.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifdef TOOLS_ENABLED
#include "gdextension_interface_header_generator.h"
#include "core/io/json.h"
#include "gdextension_interface_dump.gen.h"
static const char *FILE_HEADER =
"/**************************************************************************/\n"
"/* gdextension_interface.h */\n";
static const char *INTRO =
"\n"
"#pragma once\n"
"\n"
"/* This is a C class header, you can copy it and use it directly in your own binders.\n"
" * Together with the `extension_api.json` file, you should be able to generate any binder.\n"
" */\n"
"\n"
"#ifndef __cplusplus\n"
"#include <stddef.h>\n"
"#include <stdint.h>\n"
"\n"
"typedef uint32_t char32_t;\n"
"typedef uint16_t char16_t;\n"
"#else\n"
"#include <cstddef>\n"
"#include <cstdint>\n"
"\n"
"extern \"C\" {\n"
"#endif\n"
"\n";
static const char *OUTRO =
"#ifdef __cplusplus\n"
"}\n"
"#endif\n";
void GDExtensionInterfaceHeaderGenerator::generate_gdextension_interface_header(const String &p_path) {
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_MSG(fa.is_null(), vformat("Cannot open file '%s' for writing.", p_path));
Vector<uint8_t> bytes = GDExtensionInterfaceDump::load_gdextension_interface_file();
String json_string = String::utf8(Span<char>((char *)bytes.ptr(), bytes.size()));
Ref<JSON> json;
json.instantiate();
Error err = json->parse(json_string);
ERR_FAIL_COND(err);
Dictionary data = json->get_data();
ERR_FAIL_COND(data.is_empty());
fa->store_string(FILE_HEADER);
Array copyright = data["_copyright"];
for (const Variant &line : copyright) {
fa->store_line(line);
}
fa->store_string(INTRO);
Array types = data["types"];
for (Dictionary type_dict : types) {
if (type_dict.has("description")) {
write_doc(fa, type_dict["description"]);
}
String kind = type_dict["kind"];
if (kind == "handle") {
type_dict["type"] = type_dict.get("const", false) ? "const void*" : "void*";
write_simple_type(fa, type_dict);
} else if (kind == "alias") {
write_simple_type(fa, type_dict);
} else if (kind == "enum") {
write_enum_type(fa, type_dict);
} else if (kind == "function") {
write_function_type(fa, type_dict);
} else if (kind == "struct") {
write_struct_type(fa, type_dict);
}
}
Array interfaces = data["interface"];
for (const Variant &interface : interfaces) {
write_interface(fa, interface);
}
fa->store_string(OUTRO);
}
void GDExtensionInterfaceHeaderGenerator::write_doc(const Ref<FileAccess> &p_fa, const Array &p_doc, const String &p_indent) {
if (p_doc.size() == 1) {
p_fa->store_string(vformat("%s/* %s */\n", p_indent, p_doc[0]));
return;
}
bool first = true;
for (const Variant &line : p_doc) {
if (first) {
p_fa->store_string(p_indent + "/*");
first = false;
} else {
p_fa->store_string(p_indent + " *");
}
if (line == "") {
p_fa->store_string("\n");
} else {
p_fa->store_line(String(" ") + (String)line);
}
}
p_fa->store_string(p_indent + " */\n");
}
void GDExtensionInterfaceHeaderGenerator::write_simple_type(const Ref<FileAccess> &p_fa, const Dictionary &p_type) {
String type_and_name = format_type_and_name(p_type["type"], p_type["name"]);
p_fa->store_string(vformat("typedef %s;%s\n", type_and_name, make_deprecated_note(p_type)));
}
void GDExtensionInterfaceHeaderGenerator::write_enum_type(const Ref<FileAccess> &p_fa, const Dictionary &p_enum) {
p_fa->store_string("typedef enum {\n");
Array values = p_enum["values"];
for (Dictionary value_dict : values) {
if (value_dict.has("description")) {
write_doc(p_fa, value_dict["description"], "\t");
}
p_fa->store_string(vformat("\t%s = %s,\n", value_dict["name"], (int)value_dict["value"]));
}
p_fa->store_string(vformat("} %s;%s\n\n", p_enum["name"], make_deprecated_note(p_enum)));
}
void GDExtensionInterfaceHeaderGenerator::write_function_type(const Ref<FileAccess> &p_fa, const Dictionary &p_func) {
String args_text = p_func.has("arguments") ? make_args_text(p_func["arguments"]) : "";
String name_and_args = vformat("(*%s)(%s)", p_func["name"], args_text);
Dictionary ret = p_func["return_value"];
p_fa->store_string(vformat("typedef %s;%s\n", format_type_and_name(ret["type"], name_and_args), make_deprecated_note(p_func)));
}
void GDExtensionInterfaceHeaderGenerator::write_struct_type(const Ref<FileAccess> &p_fa, const Dictionary &p_struct) {
p_fa->store_string("typedef struct {\n");
Array members = p_struct["members"];
for (Dictionary member_dict : members) {
if (member_dict.has("description")) {
write_doc(p_fa, member_dict["description"], "\t");
}
p_fa->store_string(vformat("\t%s;\n", format_type_and_name(member_dict["type"], member_dict["name"])));
}
p_fa->store_string(vformat("} %s;%s\n\n", p_struct["name"], make_deprecated_note(p_struct)));
}
String GDExtensionInterfaceHeaderGenerator::format_type_and_name(const String &p_type, const String &p_name) {
String ret = p_type;
bool is_pointer = false;
if (ret.ends_with("*")) {
ret = ret.substr(0, ret.size() - 2) + " *";
is_pointer = true;
}
if (!p_name.is_empty()) {
if (is_pointer) {
ret = ret + p_name;
} else {
ret = ret + " " + p_name;
}
}
return ret;
}
String GDExtensionInterfaceHeaderGenerator::make_deprecated_note(const Dictionary &p_type) {
if (!p_type.has("deprecated")) {
return "";
}
return vformat(" /* %s */", p_type["deprecated"]);
}
String GDExtensionInterfaceHeaderGenerator::make_args_text(const Array &p_args) {
Vector<String> combined;
for (Dictionary arg_dict : p_args) {
combined.push_back(format_type_and_name(arg_dict["type"], arg_dict.get("name", String())));
}
return String(", ").join(combined);
}
void GDExtensionInterfaceHeaderGenerator::write_interface(const Ref<FileAccess> &p_fa, const Dictionary &p_interface) {
Vector<String> doc;
doc.push_back(String("@name ") + (String)p_interface["name"]);
doc.push_back(String("@since ") + (String)p_interface["since"]);
if (p_interface.has("deprecated")) {
String deprecated = p_interface["deprecated"];
if (deprecated.to_lower().begins_with("deprecated")) {
Vector<String> parts = deprecated.split_spaces(1);
deprecated = parts[1];
}
doc.push_back(String("@deprecated ") + deprecated);
}
Array orig_doc = p_interface["description"];
for (int i = 0; i < orig_doc.size(); i++) {
// Put an empty line before the 1st and 2nd lines.
if (i <= 1) {
doc.push_back("");
}
doc.push_back(orig_doc[i]);
}
if (p_interface.has("arguments")) {
Array args = p_interface["arguments"];
if (args.size() > 0) {
doc.push_back("");
for (Dictionary arg_dict : args) {
String arg_string = String("@param ") + (String)arg_dict["name"];
if (arg_dict.has("description")) {
Array arg_doc = arg_dict["description"];
for (const Variant &d : arg_doc) {
arg_string += String(" ") + (String)d;
}
}
doc.push_back(arg_string);
}
}
}
if (p_interface.has("return_value")) {
Dictionary ret = p_interface["return_value"];
if (ret["type"] != "void") {
String ret_string = String("@return");
if (ret.has("description")) {
Array arg_doc = ret["description"];
for (const Variant &d : arg_doc) {
ret_string += String(" ") + (String)d;
}
}
doc.push_back("");
doc.push_back(ret_string);
}
}
if (p_interface.has("see")) {
Array see_array = p_interface["see"];
if (see_array.size() > 0) {
doc.push_back("");
for (const Variant &see : see_array) {
doc.push_back(String("@see ") + (String)see);
}
}
}
p_fa->store_string("/**\n");
for (const String &d : doc) {
if (d == "") {
p_fa->store_string(" *\n");
} else {
p_fa->store_string(vformat(" * %s\n", d));
}
}
p_fa->store_string(" */\n");
Dictionary func = p_interface.duplicate();
func.erase("deprecated");
if (p_interface.has("legacy_type_name")) {
// @todo When we can break compat, remove this! This maintains legacy type-o's in some type names.
func["name"] = p_interface["legacy_type_name"];
} else {
// Cannot use `to_pascal_case()` because it'll capitalize after numbers.
Vector<String> words = ((String)p_interface["name"]).split("_");
for (String &word : words) {
// Cannot use `capitalize()` on the whole string, because it'll separate numbers with a space.
word[0] = String::char_uppercase(word[0]);
}
func["name"] = String("GDExtensionInterface") + String().join(words);
}
write_function_type(p_fa, func);
p_fa->store_string("\n");
}
#endif // TOOLS_ENABLED

View File

@@ -0,0 +1,55 @@
/**************************************************************************/
/* gdextension_interface_header_generator.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#ifdef TOOLS_ENABLED
#include "core/io/file_access.h"
class GDExtensionInterfaceHeaderGenerator {
public:
static void generate_gdextension_interface_header(const String &p_path);
private:
static void write_doc(const Ref<FileAccess> &p_fa, const Array &p_doc, const String &p_indent = "");
static void write_simple_type(const Ref<FileAccess> &p_fa, const Dictionary &p_type);
static void write_enum_type(const Ref<FileAccess> &p_fa, const Dictionary &p_enum);
static void write_function_type(const Ref<FileAccess> &p_fa, const Dictionary &p_func);
static void write_struct_type(const Ref<FileAccess> &p_fa, const Dictionary &p_struct);
static String format_type_and_name(const String &p_type, const String &p_name);
static String make_deprecated_note(const Dictionary &p_type);
static String make_args_text(const Array &p_args);
static void write_interface(const Ref<FileAccess> &p_fa, const Dictionary &p_interface);
};
#endif // TOOLS_ENABLED

View File

@@ -30,7 +30,7 @@
#pragma once #pragma once
#include "core/extension/gdextension_interface.h" #include "core/extension/gdextension_interface.gen.h"
#include "core/object/class_db.h" #include "core/object/class_db.h"
class GodotInstance : public Object { class GodotInstance : public Object {

View File

@@ -22,15 +22,22 @@ inline constexpr unsigned char _gdextension_interface_data_compressed[] = {{
class GDExtensionInterfaceDump {{ class GDExtensionInterfaceDump {{
public: public:
static void generate_gdextension_interface_file(const String &p_path) {{ static Vector<uint8_t> load_gdextension_interface_file() {{
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_MSG(fa.is_null(), vformat("Cannot open file '%s' for writing.", p_path));
Vector<uint8_t> data; Vector<uint8_t> data;
data.resize(_gdextension_interface_data_uncompressed_size); data.resize(_gdextension_interface_data_uncompressed_size);
int ret = Compression::decompress(data.ptrw(), _gdextension_interface_data_uncompressed_size, _gdextension_interface_data_compressed, _gdextension_interface_data_compressed_size, Compression::MODE_DEFLATE); int ret = Compression::decompress(data.ptrw(), _gdextension_interface_data_uncompressed_size, _gdextension_interface_data_compressed, _gdextension_interface_data_compressed_size, Compression::MODE_DEFLATE);
ERR_FAIL_COND_MSG(ret == -1, "Compressed file is corrupt."); ERR_FAIL_COND_V_MSG(ret == -1, Vector<uint8_t>(), "Compressed file is corrupt.");
fa->store_buffer(data.ptr(), data.size()); return data;
}}; }}
static void generate_gdextension_interface_file(const String &p_path) {{
Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::WRITE);
ERR_FAIL_COND_MSG(fa.is_null(), vformat("Cannot open file '%s' for writing.", p_path));
Vector<uint8_t> data = load_gdextension_interface_file();
if (data.size() > 0) {{
fa->store_buffer(data.ptr(), data.size());
}}
}}
}}; }};
#endif // TOOLS_ENABLED #endif // TOOLS_ENABLED

View File

@@ -0,0 +1,306 @@
import difflib
import json
from collections import OrderedDict
import methods
BASE_TYPES = [
"void",
"int",
"int8_t",
"uint8_t",
"int16_t",
"uint16_t",
"int32_t",
"uint32_t",
"int64_t",
"uint64_t",
"size_t",
"char",
"char16_t",
"char32_t",
"wchar_t",
"float",
"double",
]
def run(target, source, env):
filename = str(source[0])
buffer = methods.get_buffer(filename)
data = json.loads(buffer, object_pairs_hook=OrderedDict)
check_formatting(buffer.decode("utf-8"), data, filename)
check_allowed_keys(data, ["_copyright", "$schema", "format_version", "types", "interface"])
valid_data_types = {}
for type in BASE_TYPES:
valid_data_types[type] = True
with methods.generated_wrapper(str(target[0])) as file:
file.write("""\
#ifndef __cplusplus
#include <stddef.h>
#include <stdint.h>
typedef uint32_t char32_t;
typedef uint16_t char16_t;
#else
#include <cstddef>
#include <cstdint>
extern "C" {
#endif
""")
handles = []
for type in data["types"]:
kind = type["kind"]
check_type(kind, type, valid_data_types)
valid_data_types[type["name"]] = True
if "description" in type:
write_doc(file, type["description"])
if kind == "handle":
check_allowed_keys(type, ["name", "kind"], ["const", "parent", "description", "deprecated"])
if "parent" in type and type["parent"] not in handles:
raise UnknownTypeError(type["parent"], type["name"])
# @todo In the future, let's write these as `struct *` so the compiler can help us with type checking.
type["type"] = "void*" if not type.get("const", False) else "const void*"
write_simple_type(file, type)
handles.append(type["name"])
elif kind == "alias":
check_allowed_keys(type, ["name", "kind", "type"], ["description", "deprecated"])
write_simple_type(file, type)
elif kind == "enum":
check_allowed_keys(type, ["name", "kind", "values"], ["description", "deprecated"])
write_enum_type(file, type)
elif kind == "function":
check_allowed_keys(type, ["name", "kind", "return_value", "arguments"], ["description", "deprecated"])
write_function_type(file, type)
elif kind == "struct":
check_allowed_keys(type, ["name", "kind", "members"], ["description", "deprecated"])
write_struct_type(file, type)
else:
raise Exception(f"Unknown kind of type: {kind}")
for interface in data["interface"]:
check_type("function", interface, valid_data_types)
check_allowed_keys(
interface,
["name", "return_value", "arguments", "since", "description"],
["see", "legacy_type_name", "deprecated"],
)
write_interface(file, interface)
file.write("""\
#ifdef __cplusplus
}
#endif
""")
# Serialize back into JSON in order to see if the formatting remains the same.
def check_formatting(buffer, data, filename):
buffer2 = json.dumps(data, indent=4)
lines1 = buffer.splitlines()
lines2 = buffer2.splitlines()
diff = difflib.unified_diff(
lines1,
lines2,
fromfile="a/" + filename,
tofile="b/" + filename,
lineterm="",
)
diff = list(diff)
if len(diff) > 0:
print(" *** Apply this patch to fix: ***\n")
print("\n".join(diff))
raise Exception(f"Formatting issues in {filename}")
def check_allowed_keys(data, required, optional=[]):
keys = data.keys()
allowed = required + optional
for k in keys:
if k not in allowed:
raise Exception(f"Found unknown key '{k}'")
for r in required:
if r not in keys:
raise Exception(f"Missing required key '{r}'")
class UnknownTypeError(Exception):
def __init__(self, unknown, parent, item=None):
self.unknown = unknown
self.parent = parent
if item:
msg = f"Unknown type '{unknown}' for '{item}' used in '{parent}'"
else:
msg = f"Unknown type '{unknown}' used in '{parent}'"
super().__init__(msg)
def base_type_name(type_name):
if type_name.startswith("const "):
type_name = type_name[6:]
if type_name.endswith("*"):
type_name = type_name[:-1]
return type_name
def format_type_and_name(type, name=None):
ret = type
if ret[-1] == "*":
ret = ret[:-1] + " *"
if name:
if ret[-1] == "*":
ret = ret + name
else:
ret = ret + " " + name
return ret
def check_type(kind, type, valid_data_types):
if kind == "alias":
if base_type_name(type["type"]) not in valid_data_types:
raise UnknownTypeError(type["type"], type["name"])
elif kind == "struct":
for member in type["members"]:
if base_type_name(member["type"]) not in valid_data_types:
raise UnknownTypeError(member["type"], type["name"], member["name"])
elif kind == "function":
for arg in type["arguments"]:
if base_type_name(arg["type"]) not in valid_data_types:
raise UnknownTypeError(arg["type"], type["name"], arg.get("name"))
if "return_value" in type:
if base_type_name(type["return_value"]["type"]) not in valid_data_types:
raise UnknownTypeError(type["return_value"]["type"], type["name"])
def write_doc(file, doc, indent=""):
if len(doc) == 1:
file.write(f"{indent}/* {doc[0]} */\n")
return
first = True
for line in doc:
if first:
file.write(indent + "/*")
first = False
else:
file.write(indent + " *")
if line != "":
file.write(" " + line)
file.write("\n")
file.write(indent + " */\n")
def make_deprecated_note(type):
if "deprecated" not in type:
return ""
return f" /* {type['deprecated']} */"
def write_simple_type(file, type):
file.write(f"typedef {format_type_and_name(type['type'], type['name'])};{make_deprecated_note(type)}\n")
def write_enum_type(file, enum):
file.write("typedef enum {\n")
for value in enum["values"]:
check_allowed_keys(value, ["name", "value"], ["description", "deprecated"])
if "description" in value:
write_doc(file, value["description"], "\t")
file.write(f"\t{value['name']} = {value['value']},\n")
file.write(f"}} {enum['name']};{make_deprecated_note(enum)}\n\n")
def make_args_text(args):
combined = []
for arg in args:
check_allowed_keys(arg, ["type"], ["name", "description"])
combined.append(format_type_and_name(arg["type"], arg.get("name")))
return ", ".join(combined)
def write_function_type(file, fn):
args_text = make_args_text(fn["arguments"]) if ("arguments" in fn) else ""
name_and_args = f"(*{fn['name']})({args_text})"
file.write(
f"typedef {format_type_and_name(fn['return_value']['type'], name_and_args)};{make_deprecated_note(fn)}\n"
)
def write_struct_type(file, struct):
file.write("typedef struct {\n")
for member in struct["members"]:
check_allowed_keys(member, ["name", "type"], ["description"])
if "description" in member:
write_doc(file, member["description"], "\t")
file.write(f"\t{format_type_and_name(member['type'], member['name'])};\n")
file.write(f"}} {struct['name']};{make_deprecated_note(struct)}\n\n")
def write_interface(file, interface):
doc = [
f"@name {interface['name']}",
f"@since {interface['since']}",
]
if "deprecated" in interface:
if interface["deprecated"].lower().startswith("deprecated "):
parts = interface["deprecated"].split(" ", 1)
interface["deprecated"] = parts[1]
doc.append(f"@deprecated {interface['deprecated']}")
doc += [
"",
interface["description"][0],
]
if len(interface["description"]) > 1:
doc.append("")
doc += interface["description"][1:]
if "arguments" in interface:
doc.append("")
for arg in interface["arguments"]:
if "description" not in arg:
raise Exception(f"Interface function {interface['name']} is missing docs for {arg['name']} argument")
arg_doc = " ".join(arg["description"])
doc.append(f"@param {arg['name']} {arg_doc}")
if "return_value" in interface and interface["return_value"]["type"] != "void":
if "description" not in interface["return_value"]:
raise Exception(f"Interface function {interface['name']} is missing docs for return value")
ret_doc = " ".join(interface["return_value"]["description"])
doc.append("")
doc.append(f"@return {ret_doc}")
if "see" in interface:
doc.append("")
for see in interface["see"]:
doc.append(f"@see {see}")
file.write("/**\n")
for d in doc:
if d != "":
file.write(f" * {d}\n")
else:
file.write(" *\n")
file.write(" */\n")
fn = interface.copy()
if "deprecated" in fn:
del fn["deprecated"]
fn["name"] = "GDExtensionInterface" + "".join(word.capitalize() for word in interface["name"].split("_"))
write_function_type(file, fn)
file.write("\n")

View File

@@ -30,7 +30,7 @@
#pragma once #pragma once
#include "core/extension/gdextension_interface.h" #include "core/extension/gdextension_interface.gen.h"
#include "core/object/gdtype.h" #include "core/object/gdtype.h"
#include "core/object/message_queue.h" #include "core/object/message_queue.h"
#include "core/object/object_id.h" #include "core/object/object_id.h"

View File

@@ -36,6 +36,7 @@
#include "core/debugger/engine_debugger.h" #include "core/debugger/engine_debugger.h"
#include "core/extension/extension_api_dump.h" #include "core/extension/extension_api_dump.h"
#include "core/extension/gdextension_interface_dump.gen.h" #include "core/extension/gdextension_interface_dump.gen.h"
#include "core/extension/gdextension_interface_header_generator.h"
#include "core/extension/gdextension_manager.h" #include "core/extension/gdextension_manager.h"
#include "core/input/input.h" #include "core/input/input.h"
#include "core/input/input_map.h" #include "core/input/input_map.h"
@@ -278,6 +279,7 @@ static bool print_fps = false;
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
static bool editor_pseudolocalization = false; static bool editor_pseudolocalization = false;
static bool dump_gdextension_interface = false; static bool dump_gdextension_interface = false;
static bool dump_gdextension_interface_header = false;
static bool dump_extension_api = false; static bool dump_extension_api = false;
static bool include_docs_in_extension_api_dump = false; static bool include_docs_in_extension_api_dump = false;
static bool validate_extension_api = false; static bool validate_extension_api = false;
@@ -697,6 +699,7 @@ void Main::print_help(const char *p_binary) {
#endif #endif
print_help_option("--build-solutions", "Build the scripting solutions (e.g. for C# projects). Implies --editor and requires a valid project to edit.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--build-solutions", "Build the scripting solutions (e.g. for C# projects). Implies --editor and requires a valid project to edit.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--dump-gdextension-interface", "Generate a GDExtension header file \"gdextension_interface.h\" in the current folder. This file is the base file required to implement a GDExtension.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--dump-gdextension-interface", "Generate a GDExtension header file \"gdextension_interface.h\" in the current folder. This file is the base file required to implement a GDExtension.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--dump-gdextension-interface-json", "Generate a JSON dump of the GDExtension interface named \"gdextension_interface.json\" in the current folder.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--dump-extension-api", "Generate a JSON dump of the Godot API for GDExtension bindings named \"extension_api.json\" in the current folder.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--dump-extension-api", "Generate a JSON dump of the Godot API for GDExtension bindings named \"extension_api.json\" in the current folder.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--dump-extension-api-with-docs", "Generate JSON dump of the Godot API like the previous option, but including documentation.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--dump-extension-api-with-docs", "Generate JSON dump of the Godot API like the previous option, but including documentation.\n", CLI_OPTION_AVAILABILITY_EDITOR);
print_help_option("--validate-extension-api <path>", "Validate an extension API file dumped (with one of the two previous options) from a previous version of the engine to ensure API compatibility.\n", CLI_OPTION_AVAILABILITY_EDITOR); print_help_option("--validate-extension-api <path>", "Validate an extension API file dumped (with one of the two previous options) from a previous version of the engine to ensure API compatibility.\n", CLI_OPTION_AVAILABILITY_EDITOR);
@@ -1525,12 +1528,22 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
// Register as an editor instance to use low-end fallback if relevant. // Register as an editor instance to use low-end fallback if relevant.
editor = true; editor = true;
cmdline_tool = true; cmdline_tool = true;
dump_gdextension_interface = true; dump_gdextension_interface_header = true;
print_line("Dumping GDExtension interface header file"); print_line("Dumping GDExtension interface header file");
// Hack. Not needed but otherwise we end up detecting that this should // Hack. Not needed but otherwise we end up detecting that this should
// run the project instead of a cmdline tool. // run the project instead of a cmdline tool.
// Needs full refactoring to fix properly. // Needs full refactoring to fix properly.
main_args.push_back(arg); main_args.push_back(arg);
} else if (arg == "--dump-gdextension-interface-json") {
// Register as an editor instance to use low-end fallback if relevant.
editor = true;
cmdline_tool = true;
dump_gdextension_interface = true;
print_line("Dumping GDExtension interface json file");
// Hack. Not needed but otherwise we end up detecting that this should
// run the project instead of a cmdline tool.
// Needs full refactoring to fix properly.
main_args.push_back(arg);
} else if (arg == "--dump-extension-api") { } else if (arg == "--dump-extension-api") {
// Register as an editor instance to use low-end fallback if relevant. // Register as an editor instance to use low-end fallback if relevant.
editor = true; editor = true;
@@ -4041,7 +4054,11 @@ int Main::start() {
// GDExtension API and interface. // GDExtension API and interface.
{ {
if (dump_gdextension_interface) { if (dump_gdextension_interface) {
GDExtensionInterfaceDump::generate_gdextension_interface_file("gdextension_interface.h"); GDExtensionInterfaceDump::generate_gdextension_interface_file("gdextension_interface.json");
}
if (dump_gdextension_interface_header) {
GDExtensionInterfaceHeaderGenerator::generate_gdextension_interface_header("gdextension_interface.h");
} }
if (dump_extension_api) { if (dump_extension_api) {
@@ -4049,7 +4066,7 @@ int Main::start() {
GDExtensionAPIDump::generate_extension_json_file("extension_api.json", include_docs_in_extension_api_dump); GDExtensionAPIDump::generate_extension_json_file("extension_api.json", include_docs_in_extension_api_dump);
} }
if (dump_gdextension_interface || dump_extension_api) { if (dump_gdextension_interface || dump_gdextension_interface_header || dump_extension_api) {
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }

View File

@@ -30,7 +30,7 @@
#pragma once #pragma once
#include "core/extension/gdextension_interface.h" #include "core/extension/gdextension_interface.gen.h"
enum ModuleInitializationLevel { enum ModuleInitializationLevel {
MODULE_INITIALIZATION_LEVEL_CORE = GDEXTENSION_INITIALIZATION_CORE, MODULE_INITIALIZATION_LEVEL_CORE = GDEXTENSION_INITIALIZATION_CORE,