Files
Ole André Vadla Ravnås ee92ba7dea Wire up some more
2026-01-18 11:17:14 +01:00

1375 lines
40 KiB
Python

from __future__ import annotations
import xml.etree.ElementTree as ET
from collections import OrderedDict, defaultdict
from dataclasses import dataclass, field
from enum import Enum
from functools import cached_property
from typing import (Callable, Iterator, List, Mapping, Optional, Sequence,
Tuple, Union)
CORE_NAMESPACE = "http://www.gtk.org/introspection/core/1.0"
C_NAMESPACE = "http://www.gtk.org/introspection/c/1.0"
GLIB_NAMESPACE = "http://www.gtk.org/introspection/glib/1.0"
GIR_NAMESPACES = {"": CORE_NAMESPACE, "glib": GLIB_NAMESPACE}
CORE_TAG_PREFIX = f"{{{CORE_NAMESPACE}}}"
NUMERIC_GIR_TYPES = {
"gsize",
"gssize",
"gint",
"guint",
"glong",
"gulong",
"gint8",
"gint16",
"gint32",
"gint64",
"guint8",
"guint16",
"guint32",
"guint64",
"GType",
"GQuark",
}
PRIMITIVE_GIR_TYPES = NUMERIC_GIR_TYPES | {
"gpointer",
"gboolean",
"gchar",
"utf8",
"utf8[]",
}
ResolveTypeCallback = Callable[[str], Tuple[str, ET.Element]]
@dataclass
class Customizations:
custom_types: Mapping[str, CustomType] = field(default_factory=OrderedDict)
type_customizations: Mapping[str, TypeCustomizations] = field(
default_factory=OrderedDict
)
facade_exports: List[str] = field(default_factory=list)
facade_code: str = ""
helper_imports: List[str] = field(default_factory=list)
helper_code: str = ""
@dataclass
class Model:
namespace: Namespace
_object_types: OrderedDict[str, ObjectType]
enumerations: OrderedDict[str, Enumeration]
customizations: Customizations = field(default_factory=Customizations)
@cached_property
def object_types(self) -> OrderedDict[str, ObjectType]:
result = OrderedDict()
type_customizations = self.customizations.type_customizations
for k, v in self._object_types.items():
custom = type_customizations.get(k)
if custom is None or not custom.drop:
result[k] = v
return result
@cached_property
def regular_object_types(self) -> List[ObjectType]:
return [t for t in self.object_types.values() if not t.is_frida_options and not t.is_frida_list]
@cached_property
def public_types(self) -> OrderedDict[str, Union[ObjectType, Enumeration]]:
return OrderedDict(
[(k, v) for k, v in self.object_types.items() if v.is_public]
+ list(self.enumerations.items())
)
@cached_property
def interface_types_with_abstract_base(self) -> List[InterfaceObjectType]:
return [
t
for t in self.object_types.values()
if isinstance(t, InterfaceObjectType) and t.has_abstract_base
]
def resolve_object_type(self, name: str) -> ObjectType:
bare_name = name.split(".", maxsplit=1)[-1]
return self.object_types[bare_name]
def resolve_js_type(self, t: Type) -> str:
js = js_type_from_gir(t.name)
otype = self.object_types.get(js)
if otype is not None:
return otype.js_name
return js
@dataclass
class Namespace:
name: str
identifier_prefixes: str
element: ET.Element
@cached_property
def type_elements(self) -> Mapping[str, ET.Element]:
result = {}
for toplevel in self.element.findall("./*[@name]", GIR_NAMESPACES):
name = toplevel.get("name")
result[name] = toplevel
for callback in toplevel.findall("./callback", GIR_NAMESPACES):
result[name + callback.get("name")] = callback
return result
@dataclass
class ObjectType:
name: str
c_type: str
get_type: str
type_struct: str
_parent: Optional[str]
_constructors: List[ET.Element]
_methods: List[ET.Element]
_properties: List[ET.Element]
_signals: List[ET.Element]
resolve_type: ResolveTypeCallback
model: Optional[Model]
@cached_property
def js_name(self) -> str:
custom = self.customizations
if custom is not None and custom.js_name is not None:
return custom.js_name
return self.name
@cached_property
def py_name(self) -> str:
custom = self.customizations
if custom is not None and custom.py_name is not None:
return custom.py_name
return "GObject" if self.name == "Object" else self.name
@cached_property
def prefixed_js_name(self) -> str:
return f"_{self.js_name}" if self.needs_wrapper else self.js_name
@cached_property
def abstract_base_c_type(self) -> str:
return f"PyAbstract{self.name}"
@cached_property
def parent(self) -> ObjectType:
if self._parent is None:
return None
return self.model.resolve_object_type(self._parent)
@property
def is_public(self) -> bool:
return not self.is_frida_list
@cached_property
def is_frida_options(self) -> bool:
return self.c_type.startswith("Frida") and self.c_type.endswith("Options")
@cached_property
def is_frida_list(self) -> bool:
return self.c_type.startswith("Frida") and self.c_type.endswith("List")
@cached_property
def needs_wrapper(self) -> bool:
custom = self.customizations
if custom is None:
return False
if custom.custom_code is not None:
return True
ctor = self.constructors[0] if self.constructors else None
if ctor is not None and ctor.needs_wrapper:
return True
return self.wrapped_methods or self.wrapped_signals
@cached_property
def customizations(self) -> Optional[ObjectTypeCustomizations]:
return self.model.customizations.type_customizations.get(self.name)
@cached_property
def c_symbol_prefix(self) -> str:
return f"Py{self.py_name}"
@cached_property
def parent_c_symbol_prefix(self) -> str:
parent = self.parent
return parent.c_symbol_prefix if parent is not None else "PyGObject"
@cached_property
def abstract_base_c_symbol_prefix(self) -> str:
return f"PyAbstract{self.name}"
@cached_property
def c_cast_macro(self) -> str:
return to_macro_case(self.c_type)
@cached_property
def abstract_base_c_cast_macro(self) -> str:
return to_macro_case(self.abstract_base_c_type)
@cached_property
def constructors(self) -> List[Constructor]:
constructors = []
custom = self.customizations
for element in self._constructors:
if element.get("introspectable") == "0" or element.get("deprecated") == "1":
continue
name = element.get("name")
if custom is not None:
ccust = custom.constructor
if ccust is not None and ccust.drop:
continue
(
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
) = extract_callable_details(element, element, self, self.resolve_type)
if has_closure_param or finish_c_identifier is not None:
continue
constructors.append(
Constructor(
name, c_identifier, finish_c_identifier, param_list, throws, self
)
)
return constructors
@cached_property
def methods(self) -> List[Method]:
methods = []
c_prop_names = {prop.c_name for prop in self.properties}
custom = self.customizations
for element in self._methods:
name = element.get("name")
if (
element.get("introspectable") == "0"
or name.startswith("_")
or name.endswith("_sync")
or name.endswith("_finish")
):
continue
if custom is not None:
mcust = custom.methods.get(name, None)
if mcust is not None and mcust.drop:
continue
finish_func = element.get(f"{{{GLIB_NAMESPACE}}}finish-func")
if finish_func is None:
finish_func = f"{name}_finish"
result_element = next(
(m for m in self._methods if m.get("name") == finish_func), element
)
(
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
) = extract_callable_details(
element, result_element, self, self.resolve_type
)
if has_closure_param:
continue
retval_element = result_element.find(".//return-value", GIR_NAMESPACES)
rettype = extract_type_from_entity(retval_element, self.resolve_type)
if rettype is not None:
if rettype.is_frida_options:
continue
nullable = retval_element.get("nullable") == "1"
ownership_val = retval_element.get("transfer-ownership")
transfer_ownership = (
TransferOwnership[ownership_val]
if ownership_val is not None
else TransferOwnership.none
)
retval = ReturnValue(rettype, nullable, transfer_ownership, self)
else:
retval = None
if element.get(f"{{{GLIB_NAMESPACE}}}get-property") is not None:
is_property_accessor = True
else:
tokens = name.split("_", maxsplit=1)
is_property_accessor = (
len(tokens) == 2
and tokens[0] in {"get", "set"}
and tokens[1] in c_prop_names
)
methods.append(
Method(
name,
c_identifier,
finish_c_identifier,
param_list,
throws,
retval,
is_property_accessor,
self,
)
)
return methods
@cached_property
def wrapped_methods(self) -> List[Method]:
return [m for m in self.methods if m.needs_wrapper]
@cached_property
def properties(self) -> List[Property]:
properties = []
custom = self.customizations
for element in self._properties:
name = element.get("name")
if custom is not None:
pcust = custom.properties.get(name, None)
if pcust is not None and pcust.drop:
continue
c_name = name.replace("-", "_")
type = extract_type_from_entity(element, self.resolve_type)
if type.is_frida_options:
continue
writable = element.get("writable") == "1"
construct_only = element.get("construct-only") == "1"
getter = element.get("getter")
if getter is None:
getter = f"get_{c_name}"
setter = element.get("setter")
if setter is None and writable and not construct_only:
setter = f"set_{c_name}"
properties.append(
Property(
name,
c_name,
type,
writable,
construct_only,
getter,
setter,
self,
)
)
return properties
@cached_property
def signals(self) -> List[Signal]:
signals = []
custom = self.customizations
for element in self._signals:
name = element.get("name")
if custom is not None:
scust = custom.signals.get(name, None)
if scust is not None and scust.drop:
continue
c_name = name.replace("-", "_")
param_list = extract_parameters(
element.findall("./parameters/parameter", GIR_NAMESPACES),
nullable_implies_optional=False,
object_type=self,
resolve_type=self.resolve_type,
)
signals.append(Signal(name, c_name, param_list, self))
return signals
@cached_property
def wrapped_signals(self) -> List[Signal]:
return [s for s in self.signals if s.needs_wrapper]
@dataclass
class ClassObjectType(ObjectType):
_implements: List[str]
@cached_property
def implements(self) -> List[InterfaceObjectType]:
return [self.model.resolve_object_type(i) for i in self._implements]
@dataclass
class InterfaceObjectType(ObjectType):
@cached_property
def has_abstract_base(self) -> bool:
custom = self.customizations
if custom is None:
return True
return not custom.drop_abstract_base
@dataclass
class Procedure:
name: str
c_identifier: str
finish_c_identifier: Optional[str]
parameters: List[Parameter]
throws: bool
@property
def is_async(self) -> bool:
return self.finish_c_identifier is not None
@cached_property
def input_parameters(self) -> List[Parameter]:
return [p for p in self.parameters if p.direction != Direction.OUT]
@dataclass
class Constructor(Procedure):
object_type: ObjectType
@cached_property
def param_typings(self) -> List[str]:
custom = self.customizations
if custom is not None and custom.param_typings is not None:
return custom.param_typings
return [param.typing for param in self.parameters]
@property
def needs_wrapper(self) -> bool:
custom = self.customizations
if custom is None:
return False
return custom.custom_logic is not None
@cached_property
def customizations(self) -> Optional[ConstructorCustomizations]:
custom = self.object_type.customizations
if custom is None:
return None
return custom.constructor
@dataclass
class Method(Procedure):
return_value: Optional[ReturnValue]
is_property_accessor: bool
object_type: ObjectType
@cached_property
def js_name(self) -> str:
custom = self.customizations
if custom is not None and custom.js_name is not None:
return custom.js_name
return to_camel_case(self.name)
@cached_property
def prefixed_js_name(self) -> str:
custom = self.customizations
if self.needs_wrapper or (custom is not None and custom.hide):
return f"_{self.js_name}"
return self.js_name
@cached_property
def cself_name(self) -> str:
return to_snake_case(self.object_type.name).split("_")[-1]
@cached_property
def param_ctypings(self) -> List[str]:
result = [f"{self.object_type.c_type} * {self.cself_name}"]
result += [param.ctyping for param in self.parameters]
if self.is_async:
result += ["GAsyncReadyCallback callback", "gpointer user_data"]
return result
@cached_property
def finish_param_ctypings(self) -> List[str]:
result = [
f"{self.object_type.c_type} * {self.cself_name}",
"GAsyncResult * result",
]
if self.throws:
result.append("GError ** error")
return result
@cached_property
def param_typings(self) -> List[str]:
custom = self.customizations
if custom is not None and custom.param_typings is not None:
return custom.param_typings
return self.prefixed_param_typings
@cached_property
def prefixed_param_typings(self) -> List[str]:
return [param.typing for param in self.input_parameters]
@cached_property
def return_ctyping(self) -> str:
retval = self.return_value
return retval.ctyping if retval is not None else "void"
@cached_property
def return_typing(self) -> str:
custom = self.customizations
if custom is not None and custom.return_typing is not None:
return custom.return_typing
return self.prefixed_return_typing
@cached_property
def prefixed_return_typing(self) -> str:
retval = self.return_value
typing = retval.typing if retval is not None else "void"
return f"Promise<{typing}>" if self.is_async else typing
@property
def needs_wrapper(self) -> bool:
custom = self.customizations
if custom is None:
return False
return custom.custom_logic is not None or custom.return_wrapper is not None
@cached_property
def customizations(self) -> Optional[MethodCustomizations]:
custom = self.object_type.customizations
if custom is None:
return None
return custom.methods.get(self.name)
@cached_property
def operation_type_name(self) -> str:
return f"Py{self.object_type.name}{to_pascal_case(self.name)}Operation"
@cached_property
def abstract_base_operation_type_name(self) -> str:
return f"PyAbstract{self.object_type.name}{to_pascal_case(self.name)}Operation"
@cached_property
def is_select_method(self) -> bool:
return self.name.startswith("select_") or self.name.startswith("add_")
@cached_property
def select_noun(self) -> str:
assert (
self.is_select_method
), "select_noun can only be called on selector methods"
return self.name.split("_", maxsplit=1)[1]
@cached_property
def select_plural_noun(self) -> str:
return f"{self.select_noun}s"
@cached_property
def select_element_type(self) -> Type:
assert (
self.is_select_method
), "select_element_type can only be called on selector methods"
return self.parameters[0].type
@dataclass
class Property:
name: str
c_name: str
type: Type
writable: bool
construct_only: bool
getter: Optional[str]
setter: Optional[str]
object_type: ObjectType
@cached_property
def js_name(self) -> str:
custom = self.customizations
if custom is not None and custom.js_name is not None:
return custom.js_name
return to_camel_case(self.c_name)
@cached_property
def typing(self) -> str:
custom = self.customizations
if custom is not None and custom.typing is not None:
return custom.typing
readonly = "readonly " if not self.writable else ""
optional_str = "?" if self.object_type.is_frida_options else ""
return f"{readonly}{self.js_name}{optional_str}: {self.object_type.model.resolve_js_type(self.type)}"
@cached_property
def customizations(self) -> Optional[PropertyCustomizations]:
custom = self.object_type.customizations
if custom is None:
return None
return custom.properties.get(self.name)
@dataclass
class Signal:
name: str
c_name: str
parameters: List[Parameter]
object_type: ObjectType
@cached_property
def js_name(self) -> str:
return to_camel_case(self.c_name)
@cached_property
def prefixed_js_name(self) -> str:
return f"_{self.js_name}" if self.needs_wrapper else self.js_name
@cached_property
def handler_type_name(self) -> str:
# XXX: Special-cases to avoid breaking API:
class_name = self.object_type.name
if class_name == "DeviceManager":
prefix = "Device"
elif class_name == "Device":
prefix = "Device" if self.name == "lost" else ""
elif class_name == "PortalService":
prefix = "Portal"
elif class_name == "Cancellable":
prefix = ""
else:
prefix = class_name
return f"{prefix}{to_pascal_case(self.c_name)}Handler"
@cached_property
def prefixed_handler_type_name(self) -> str:
return (
f"_{self.handler_type_name}"
if self.needs_wrapper
else self.handler_type_name
)
@cached_property
def typing(self) -> str:
params = ", ".join([p.typing for p in self.parameters])
return f"({params}) => void"
@property
def needs_wrapper(self) -> bool:
custom = self.customizations
if custom is None:
return False
return custom.transform is not None or custom.intercept is not None
@cached_property
def customizations(self) -> Optional[SignalCustomizations]:
custom = self.object_type.customizations
if custom is None:
return None
return custom.signals.get(self.name)
TransferOwnership = Enum("TransferOwnership", ["none", "full", "container"])
@dataclass
class Parameter:
name: str
type: Type
optional: bool
nullable: bool
transfer_ownership: TransferOwnership
direction: Direction
object_type: ObjectType
@cached_property
def js_name(self) -> str:
return to_camel_case(self.name)
@cached_property
def ctyping(self) -> str:
return f"{self.type.c} {self.name}"
@cached_property
def typing(self) -> str:
optional_str = "?" if self.optional else ""
t = f"{self.js_name}{optional_str}: {self.object_type.model.resolve_js_type(self.type)}"
if self.nullable and not self.type.is_frida_options:
t += " | null"
return t
@cached_property
def copy_func(self) -> Optional[str]:
return self.type.copy_func
@cached_property
def destroy_func(self) -> Optional[str]:
return self.type.destroy_func
@dataclass
class ReturnValue:
type: Type
nullable: bool
transfer_ownership: TransferOwnership
object_type: ObjectType
@cached_property
def ctyping(self) -> str:
return self.type.c
@cached_property
def typing(self) -> str:
t = self.object_type.model.resolve_js_type(self.type)
if self.nullable:
t += " | null"
return t
@cached_property
def destroy_func(self) -> Optional[str]:
if self.transfer_ownership == TransferOwnership.none:
return None
return self.type.destroy_func
@dataclass
class Type:
name: str
nick: str
c: str
default_value: Optional[str]
copy_func: Optional[str]
destroy_func: Optional[str]
@cached_property
def from_pointer_func(self) -> Optional[str]:
if self.name in {"gssize", "gsize", "glong", "gulong", "gint64", "guint64"}:
return "GPOINTER_TO_SIZE"
if self.name in {"gint", "gint8", "gint16", "gint32"}:
return "GPOINTER_TO_INT"
if self.name in {"gboolean", "guint", "guint8", "guint16", "guint32"}:
return "GPOINTER_TO_UINT"
return None
@cached_property
def to_pointer_func(self) -> Optional[str]:
if self.name in {"gssize", "gsize", "glong", "gulong", "gint64", "guint64"}:
return "GSIZE_TO_POINTER"
if self.name in {"gint", "gint8", "gint16", "gint32"}:
return "GINT_TO_POINTER"
if self.name in {"gboolean", "guint", "guint8", "guint16", "guint32"}:
return "GUINT_TO_POINTER"
return None
@cached_property
def is_frida_options(self) -> bool:
return self.c.startswith("Frida") and self.c.endswith("Options *")
class Direction(Enum):
IN = "in"
OUT = "out"
INOUT = "inout"
@dataclass
class Enumeration:
name: str
c_type: str
get_type: str
_members: List[ET.Element]
model: Optional[Model]
@property
def js_name(self) -> str:
return self.name
@property
def prefixed_js_name(self) -> str:
return self.name
@cached_property
def members(self) -> List[EnumerationMember]:
members = []
for element in self._members:
members.append(EnumerationMember(element.get("name"), self))
return members
@property
def is_frida_options(self) -> bool:
return False
@cached_property
def customizations(self) -> Optional[EnumerationCustomizations]:
return self.model.customizations.type_customizations.get(self.name)
@cached_property
def c_symbol_prefix(self) -> str:
return f"Py{self.name}"
@dataclass
class EnumerationMember:
name: str
enumeration: Enumeration
@cached_property
def js_name(self) -> str:
custom = self.customizations
if custom is not None and custom.js_name is not None:
return custom.js_name
return to_pascal_case(self.name)
@cached_property
def nick(self) -> str:
return self.name.replace("_", "-")
@cached_property
def customizations(self) -> Optional[EnumerationMemberCustomizations]:
custom = self.enumeration.customizations
if custom is None:
return None
return custom.members.get(self.name)
@dataclass
class CustomType:
kind: CustomTypeKind
typing: str
class CustomTypeKind(Enum):
TYPE = "type"
INTERFACE = "interface"
ENUM = "enum"
@dataclass
class TypeCustomizations:
pass
@dataclass
class ObjectTypeCustomizations(TypeCustomizations):
js_name: Optional[str] = None
py_name: Optional[str] = None
drop: bool = False
drop_abstract_base: bool = False
constructor: Optional[ConstructorCustomizations] = None
methods: Mapping[str, MethodCustomizations] = field(
default_factory=lambda: defaultdict(dict)
)
properties: Mapping[str, PropertyCustomizations] = field(
default_factory=lambda: defaultdict(dict)
)
signals: Mapping[str, SignalCustomizations] = field(
default_factory=lambda: defaultdict(dict)
)
custom_code: Optional[CustomCode] = None
cleanup: Optional[str] = None
keep_alive: Optional[KeepAliveCustomization] = None
@dataclass
class KeepAliveCustomization:
is_destroyed_function: str
destroy_signal_name: str
@dataclass
class ConstructorCustomizations:
drop: bool = False
param_typings: Optional[List[str]] = None
custom_logic: Optional[str] = None
@dataclass
class MethodCustomizations:
js_name: Optional[str] = None
drop: bool = False
hide: bool = False
param_typings: Optional[List[str]] = None
return_typing: Optional[str] = None
custom_logic: Optional[str] = None
return_wrapper: Optional[str] = None
return_cconversion: Optional[str] = None
ref_keep_alive: bool = False
unref_keep_alive: bool = False
@dataclass
class PropertyCustomizations:
js_name: Optional[str] = None
drop: bool = False
typing: Optional[str] = None
@dataclass
class SignalCustomizations:
drop: bool = False
behavior: str = "FDN_SIGNAL_ALLOW_EXIT"
transform: Optional[Mapping[int, Tuple[str, Optional[str]]]] = None
intercept: Optional[str] = None
@dataclass
class CustomCode:
declarations: List[CustomDeclaration] = field(default_factory=list)
methods: List[CustomMethod] = field(default_factory=list)
@dataclass
class CustomDeclaration:
typing: Optional[str]
code: str
@dataclass
class CustomMethod:
typing: Optional[str]
code: str
@dataclass
class EnumerationCustomizations(TypeCustomizations):
members: Mapping[str, EnumerationMemberCustomizations] = field(
default_factory=lambda: defaultdict(dict)
)
@dataclass
class EnumerationMemberCustomizations:
js_name: Optional[str] = None
def parse_gir(file_path: str, dependencies: Sequence[Model]) -> Model:
tree = ET.parse(file_path)
el = tree.getroot().find("./namespace", GIR_NAMESPACES)
namespace = Namespace(
el.get("name"), el.get(f"{{{C_NAMESPACE}}}identifier-prefixes"), el
)
def resolve_type(name: str) -> Tuple[str, ET.Element]:
assert (
name not in PRIMITIVE_GIR_TYPES
), f"unexpectedly asked to resolve primitive type: {name}"
tokens = name.split(".", maxsplit=1)
if len(tokens) == 2:
ns_name, bare_name = tokens
if ns_name == namespace.name:
ns = namespace
else:
ns = next(
(
dep.namespace
for dep in dependencies
if dep.namespace.name == ns_name
),
None,
)
if ns is None:
assert ns is not None, f"unable to resolve namespace {ns_name}"
else:
ns = namespace
bare_name = name
qualified_name = f"{ns.name}.{bare_name}"
element = ns.type_elements.get(bare_name)
assert element is not None, f"unable to resolve type {bare_name}"
return (qualified_name, element)
object_types = OrderedDict()
for element in namespace.element.findall("./class", GIR_NAMESPACES):
name = element.get("name")
c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
type_struct = element.get(f"{{{GLIB_NAMESPACE}}}type-struct")
if type_struct is not None:
type_struct = namespace.identifier_prefixes + type_struct
else:
type_struct = c_type + "Class"
parent = element.get("parent")
if parent is not None:
parent, _ = resolve_type(parent)
constructors = element.findall(".//constructor", GIR_NAMESPACES)
methods = element.findall(".//method", GIR_NAMESPACES)
properties = element.findall(".//property", GIR_NAMESPACES)
signals = element.findall(".//glib:signal", GIR_NAMESPACES)
implements = [
e.get("name") for e in element.findall(".//implements", GIR_NAMESPACES)
]
object_types[name] = ClassObjectType(
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
resolve_type,
None,
implements,
)
for element in namespace.element.findall("./interface", GIR_NAMESPACES):
name = element.get("name")
c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
type_struct = element.get(f"{{{GLIB_NAMESPACE}}}type-struct")
if type_struct is not None:
type_struct = namespace.identifier_prefixes + type_struct
else:
type_struct = c_type + "Iface"
prereq = element.find(".//prerequisite", GIR_NAMESPACES)
parent = prereq.get("name") if prereq is not None else None
if parent is not None:
parent, _ = resolve_type(parent)
constructors = []
methods = element.findall(".//method", GIR_NAMESPACES)
properties = element.findall(".//property", GIR_NAMESPACES)
signals = element.findall(".//glib:signal", GIR_NAMESPACES)
object_types[name] = InterfaceObjectType(
name,
c_type,
get_type,
type_struct,
parent,
constructors,
methods,
properties,
signals,
resolve_type,
None,
)
enumerations = OrderedDict()
for element in namespace.element.findall("./enumeration", GIR_NAMESPACES):
if element.get(f"{{{GLIB_NAMESPACE}}}error-domain") is not None:
continue
enum_name = element.get("name")
enum_c_type = element.get(f"{{{C_NAMESPACE}}}type")
get_type = element.get(f"{{{GLIB_NAMESPACE}}}get-type")
members = element.findall(".//member", GIR_NAMESPACES)
enumerations[enum_name] = Enumeration(
enum_name, enum_c_type, get_type, members, None
)
model = Model(namespace, object_types, enumerations)
for t in object_types.values():
t.model = model
for t in enumerations.values():
t.model = model
return model
def extract_callable_details(
element: ET.Element,
result_element: ET.Element,
object_type: ObjectType,
resolve_type: ResolveTypeCallback,
) -> Tuple[str, Optional[str], List[Parameter], bool, bool, ET.Element]:
c_identifier = element.get(f"{{{C_NAMESPACE}}}identifier")
parameters = element.findall("./parameters/parameter", GIR_NAMESPACES)
full_param_list = extract_parameters(
parameters,
nullable_implies_optional=True,
object_type=object_type,
resolve_type=resolve_type,
)
param_list = list(all_regular_parameters(full_param_list))
has_closure_param = any((param.get("closure") == "1" for param in parameters))
is_async = any(
param.type.name == "Gio.AsyncReadyCallback" for param in full_param_list
)
if not is_async:
result_element = element
finish_c_identifier = (
result_element.get(f"{{{C_NAMESPACE}}}identifier") if is_async else None
)
throws = result_element.get("throws") == "1"
return (
c_identifier,
finish_c_identifier,
param_list,
has_closure_param,
throws,
result_element,
)
def extract_parameters(
parameter_elements: List[ET.Element],
nullable_implies_optional: bool,
object_type: ObjectType,
resolve_type: ResolveTypeCallback,
) -> List[Parameter]:
entries = []
for param in parameter_elements:
nullable = param.get("nullable") == "1"
entries.append((param, nullable))
last_required_index = None
for i, (param, nullable) in enumerate(entries):
optional = nullable and nullable_implies_optional
if not optional:
last_required_index = i
param_list = []
for i, (param, nullable) in enumerate(entries):
name = param.get("name")
type = extract_type_from_entity(param, resolve_type)
if last_required_index is None or i > last_required_index:
optional = nullable and nullable_implies_optional
else:
optional = False
ownership_val = param.get("transfer-ownership")
transfer_ownership = (
TransferOwnership[ownership_val]
if ownership_val is not None
else TransferOwnership.none
)
raw_direction = param.get("direction")
direction = (
Direction(raw_direction) if raw_direction is not None else Direction.IN
)
param_list.append(
Parameter(
name,
type,
optional,
nullable,
transfer_ownership,
direction,
object_type,
)
)
return param_list
def all_regular_parameters(parameters: List[Parameter]) -> Iterator[Parameter]:
callback_index = None
for i, param in enumerate(parameters):
if param.type.name == "Gio.AsyncReadyCallback":
callback_index = i
continue
if callback_index is not None and i == callback_index + 1:
continue
yield param
def extract_type_from_entity(
parent_element: ET.Element, resolve_type: ResolveTypeCallback
) -> Optional[Type]:
child = parent_element.find("type", GIR_NAMESPACES)
if child is None:
child = parent_element.find("array", GIR_NAMESPACES)
assert child is not None
element_type = extract_type_from_entity(child, resolve_type)
if element_type.name == "utf8":
return Type(
"utf8[]",
"strv",
"gchar **",
"NULL",
"g_strdupv",
"g_strfreev",
)
elif element_type.name == "gchar":
return Type("char[]", "chararray", "gchar *", "NULL", "NULL", "NULL")
elif element_type.name == "GObject.Value":
return Type("Value[]", "valuearray", "GValue *", "NULL", "NULL", "NULL")
else:
assert (
element_type.name == "guint8"
), f"unsupported array type: {element_type.name}"
return Type("uint8[]", "bytearray", "guint8 *", "NULL", "NULL", "NULL")
return parse_type(child, resolve_type)
def parse_type(
element: ET.Element, resolve_type: ResolveTypeCallback
) -> Optional[Type]:
name = element.get("name")
assert name is not None
if name == "none":
return None
is_primitive = name in PRIMITIVE_GIR_TYPES
c_type = element.get(f"{{{C_NAMESPACE}}}type")
core_tag = None
if is_primitive:
type_element = element
if c_type is None:
c_type = name
else:
name, type_element = resolve_type(name)
if type_element.tag.startswith(CORE_TAG_PREFIX):
core_tag = type_element.tag[len(CORE_TAG_PREFIX) :]
c_type = type_element.get(f"{{{C_NAMESPACE}}}type")
if core_tag in {"class", "interface", "record"}:
c_type += "*"
nick = type_nick_from_name(name, element, resolve_type)
c = c_type.replace("*", " *")
default_value = "NULL" if "*" in c else None
if name == "utf8":
copy_func = "g_strdup"
destroy_func = "g_free"
elif name == "utf8[]":
copy_func = "g_strdupv"
destroy_func = "g_strfreev"
elif name == "GLib.HashTable":
copy_func = "g_hash_table_ref"
destroy_func = "g_hash_table_unref"
elif name == "GLib.Quark":
copy_func = None
destroy_func = None
elif name == "GObject.Value":
copy_func = "g_value_copy"
destroy_func = "g_value_reset"
elif name == "GObject.Closure":
copy_func = "g_closure_ref"
destroy_func = "g_closure_unref"
elif core_tag in {"class", "interface"}:
copy_func = "g_object_ref"
destroy_func = "g_object_unref"
elif is_primitive or core_tag in {"bitfield", "callback", "enumeration"}:
copy_func = None
destroy_func = None
else:
copy_func = type_element.get("copy-function")
destroy_func = type_element.get("free-function")
assert (
destroy_func is not None
), f"unable to resolve destroy function for {name}, core_tag={core_tag}"
return Type(name, nick, c, default_value, copy_func, destroy_func)
def type_nick_from_name(
name: str, element: ET.Element, resolve_type: ResolveTypeCallback
) -> str:
if name == "GLib.PollFD":
return "pollfd"
tokens = name.split(".", maxsplit=1)
if len(tokens) == 1:
result = tokens[0]
if result.startswith("g"):
result = result[1:]
else:
result = to_snake_case(tokens[1])
if result == "hash_table":
key_type = parse_type(element[0], resolve_type)
value_type = parse_type(element[1], resolve_type)
assert (
key_type.name == "utf8" and value_type.name == "GLib.Variant"
), "only GHashTable<string, Variant> is supported for now"
result = "vardict"
return result
def js_type_from_gir(name: str) -> str:
if name == "gboolean":
return "boolean"
if name in NUMERIC_GIR_TYPES:
return "number"
if name == "utf8":
return "string"
if name == "utf8[]":
return "string[]"
if name == "GLib.Bytes":
return "Buffer"
if name == "GLib.HashTable":
return "VariantDict"
if name == "GLib.Variant":
return "any"
if name in {"Gio.File", "Gio.TlsCertificate"}:
return "string"
if name.startswith("Frida.") and name.endswith("List"):
return name[6:-4] + "[]"
return name.split(".")[-1]
def to_snake_case(name: str) -> str:
result = []
i = 0
n = len(name)
while i < n:
if name[i].isupper():
if i > 0:
result.append("_")
start = i
if i + 1 < n and name[i + 1].islower():
while i + 1 < n and name[i + 1].islower():
i += 1
else:
while i + 1 < n and name[i + 1].isupper():
i += 1
if i + 1 < n:
i -= 1
result.append(name[start : i + 1].lower())
else:
result.append(name[i])
i += 1
return "".join(result)
def to_pascal_case(name: str) -> str:
return "".join(word.capitalize() for word in name.split("_"))
def to_camel_case(name: str) -> str:
words = name.split("_")
return words[0] + "".join(word.capitalize() for word in words[1:])
def to_macro_case(identifier: str) -> str:
result = []
for i, char in enumerate(identifier):
if char.isupper() and i != 0:
result.append("_")
result.append(char)
return "".join(result).upper()