blob: 8f376704c4bd3b8f2dcd6e90f16142ea5c99650f [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2022 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import dataclasses
import enum
import logging
import os
from typing import List, Optional, Set
from matter_idl.generators import CodeGenerator, GeneratorStorage
from matter_idl.generators.types import (BasicInteger, BasicString, FundamentalType, IdlBitmapType, IdlEnumType, IdlType,
ParseDataType, TypeLookupContext)
from matter_idl.matter_idl_types import (Attribute, Cluster, ClusterSide, Command, DataType, Field, FieldQuality, Idl, Struct,
StructTag)
from stringcase import capitalcase
@dataclasses.dataclass
class GenerateTarget:
template: str
output_name: str
@dataclasses.dataclass
class GlobalType:
name: str # java name
cpp_type: str # underlying type
# types that java should see globally
_GLOBAL_TYPES = [
GlobalType("Boolean", "bool"),
GlobalType("CharString", "const chip::CharSpan"),
GlobalType("Double", "double"),
GlobalType("Float", "float"),
GlobalType("Int8s", "int8_t"),
GlobalType("Int8u", "uint8_t"),
GlobalType("Int16s", "int16_t"),
GlobalType("Int16u", "uint16_t"),
GlobalType("Int32s", "int32_t"),
GlobalType("Int32u", "uint32_t"),
GlobalType("Int64s", "int64_t"),
GlobalType("Int64u", "uint64_t"),
GlobalType("OctetString", "const chip::ByteSpan"),
]
def _UnderlyingType(field: Field, context: TypeLookupContext) -> Optional[str]:
actual = ParseDataType(field.data_type, context)
if isinstance(actual, (IdlEnumType, IdlBitmapType)):
actual = actual.base_type
if isinstance(actual, BasicString):
if actual.is_binary:
return 'OctetString'
else:
return 'CharString'
elif isinstance(actual, BasicInteger):
if actual.is_signed:
return "Int{}s".format(actual.power_of_two_bits)
else:
return "Int{}u".format(actual.power_of_two_bits)
elif isinstance(actual, FundamentalType):
if actual == FundamentalType.BOOL:
return 'Boolean'
elif actual == FundamentalType.FLOAT:
return 'Float'
elif actual == FundamentalType.DOUBLE:
return 'Double'
else:
logging.warn('Unknown fundamental type: %r' % actual)
return None
def FieldToGlobalName(field: Field, context: TypeLookupContext) -> Optional[str]:
"""Global names are used for generic callbacks shared across
all clusters (e.g. for bool/float/uint32 and similar)
"""
if field.is_list:
return None # lists are always specific per cluster
if FieldQuality.NULLABLE & field.qualities:
return None
return _UnderlyingType(field, context)
# Based on atomicType in ZAP:
# src-electron/generator/matter/app/zap-templates/common/override.js
_KNOWN_DECODABLE_TYPES = {
'action_id': 'chip::ActionId',
'attrib_id': 'chip::AttributeId',
'cluster_id': 'chip::ClusterId',
'command_id': 'chip::CommandId',
'data_ver': 'chip::DataVersion',
'devtype_id': 'chip::DeviceTypeId',
'endpoint_no': 'chip::EndpointId',
'eui64': 'chip::NodeId',
'event_id': 'chip::EventId',
'event_no': 'chip::EventNumber',
'fabric_id': 'chip::FabricId',
'fabric_idx': 'chip::FabricIndex',
'fabric_idx': 'chip::FabricIndex',
'field_id': 'chip::FieldId',
'group_id': 'chip::GroupId',
'node_id': 'chip::NodeId',
'percent': 'chip::Percent',
'percent100ths': 'chip::Percent100ths',
'transaction_id': 'chip::TransactionId',
'vendor_id': 'chip::VendorId',
# non-named enums
'enum8': 'uint8_t',
'enum16': 'uint16_t',
'enum32': 'uint32_t',
'enum64': 'uint64_t',
}
def _CppType(field: Field, context: TypeLookupContext) -> str:
if field.data_type.name.lower() in _KNOWN_DECODABLE_TYPES:
return _KNOWN_DECODABLE_TYPES[field.data_type.name.lower()]
actual = ParseDataType(field.data_type, context)
if isinstance(actual, BasicString):
if actual.is_binary:
return 'chip::ByteSpan'
else:
return 'chip::CharSpan'
elif isinstance(actual, BasicInteger):
if actual.is_signed:
return "int{}_t".format(actual.power_of_two_bits)
else:
return "uint{}_t".format(actual.power_of_two_bits)
elif isinstance(actual, FundamentalType):
if actual == FundamentalType.BOOL:
return 'bool'
elif actual == FundamentalType.FLOAT:
return 'float'
elif actual == FundamentalType.DOUBLE:
return 'double'
else:
logging.warn('Unknown fundamental type: %r' % actual)
elif isinstance(actual, IdlType):
return f"chip::app::Clusters::{context.cluster.name}::Structs::{field.data_type.name}::DecodableType"
elif isinstance(actual, IdlBitmapType):
return f"chip::BitMask<chip::app::Clusters::{context.cluster.name}::{field.data_type.name}>"
# Handles IdlEnumType
return f"chip::app::Clusters::{context.cluster.name}::{field.data_type.name}"
def DecodableJniType(field: Field, context: TypeLookupContext) -> str:
actual = _CppType(field, context)
needsConstRef = False
if field.is_list:
needsConstRef = True
actual = f"chip::app::DataModel::DecodableList<{actual}>"
if field.is_nullable:
needsConstRef = True
actual = f"chip::app::DataModel::Nullable<{actual}>"
if needsConstRef:
actual = f"const {actual} &"
return actual
def GlobalNameToJavaName(name: str) -> str:
if name in {'Int8u', 'Int8s', 'Int16u', 'Int16s'}:
return 'Integer'
if name.startswith('Int'):
return 'Long'
# Double/Float/Booleans/CharString/OctetString
return name
def DelegatedCallbackName(attr: Attribute, context: TypeLookupContext) -> str:
"""
Figure out what callback name to use for delegate callback construction.
"""
global_name = FieldToGlobalName(attr.definition, context)
if global_name:
return 'Delegated{}AttributeCallback'.format(GlobalNameToJavaName(global_name))
return 'Delegated{}Cluster{}AttributeCallback'.format(context.cluster.name, capitalcase(attr.definition.name))
def ChipClustersCallbackName(attr: Attribute, context: TypeLookupContext) -> str:
"""
Figure out what callback name to use when building a ChipCluster.*AttributeCallback
in java codegen.
"""
global_name = FieldToGlobalName(attr.definition, context)
if global_name:
return 'ChipClusters.{}AttributeCallback'.format(GlobalNameToJavaName(global_name))
return 'ChipClusters.{}Cluster.{}AttributeCallback'.format(context.cluster.name, capitalcase(attr.definition.name))
def CallbackName(attr: Attribute, context: TypeLookupContext) -> str:
"""
Figure out what callback name to use when a variable requires a read callback.
These are split into native types, like Boolean/Float/Double/CharString, where
one callback type can support anything.
For specific types (e.g. A struct) codegen will generate its own callback name
specific to that type.
"""
global_name = FieldToGlobalName(attr.definition, context)
if global_name:
return 'CHIP{}AttributeCallback'.format(capitalcase(global_name))
return 'CHIP{}{}AttributeCallback'.format(
capitalcase(context.cluster.name),
capitalcase(attr.definition.name)
)
def CommandCallbackName(command: Command, cluster: Cluster):
if command.output_param.lower() == 'defaultsuccess':
return 'DefaultSuccess'
return '{}Cluster{}'.format(cluster.name, command.output_param)
def attributesWithSupportedCallback(attrs, context: TypeLookupContext):
for attr in attrs:
# Attributes will be generated for all types
# except non-list structures
if not attr.definition.is_list:
underlying = ParseDataType(attr.definition.data_type, context)
if isinstance(underlying, IdlType):
continue
yield attr
def _IsUsingGlobalCallback(field: Field, context: TypeLookupContext):
"""Test to determine if the data type of a field can use one of
the global callbacks (i.e. it is a basic double/integer/bool etc.)
"""
if field.is_list:
return False
if field.is_nullable:
return False
return field.data_type.name in {
"boolean",
"single",
"double",
"int8s",
"int8u",
"int16s",
"int16u",
"int24s",
"int24u",
"int32s",
"int32u",
"int40s",
"int40u",
"int48s",
"int48u",
"int56s",
"int56u",
"int64s",
"int64u",
"enum8",
"enum16",
"enum32",
"enum64",
"bitmap8",
"bitmap16",
"bitmap32",
"bitmap64",
"char_string",
"long_char_string",
"octet_string",
"long_octet_string",
}
def NamedFilter(choices: List, name: str):
for choice in choices:
if choice.name == name:
return choice
raise Exception("No item named %s in %r" % (name, choices))
def ToBoxedJavaType(field: Field):
if field.is_optional:
return 'jobject'
elif field.data_type.name.lower() in ['octet_string', 'long_octet_string']:
return 'jbyteArray'
elif field.data_type.name.lower() in ['char_string', 'long_char_string']:
return 'jstring'
else:
return 'jobject'
def LowercaseFirst(name: str) -> str:
"""
Change the first letter of a string to lowercase as long as the 2nd
letter is not uppercase.
Can be used for variable naming, eg insider structures, codegen will
call things "Foo foo" (notice variable name is lowercase).
"""
if len(name) > 1 and name[1].lower() != name[1]:
# Odd workaround: PAKEVerifier should not become pAKEVerifier
return name
return name[0].lower() + name[1:]
class EncodableValueAttr(enum.Enum):
LIST = enum.auto()
NULLABLE = enum.auto()
OPTIONAL = enum.auto()
class EncodableValue:
"""
Contains helpers for encoding values, specifically lookups
for optionality, lists and recursive data type lookups within
the IDL and cluster
Intended use is to be able to:
- derive types (see clone and without_* methods) such that codegen
can implement things like 'if x != null { treat non-null x}'
- Java specific conversions: get boxed types and JNI string signautes
for the underlying types.
"""
def __init__(self, context: TypeLookupContext, data_type: DataType, attrs: Set[EncodableValueAttr]):
self.context = context
self.data_type = data_type
self.attrs = attrs
@property
def is_nullable(self):
return EncodableValueAttr.NULLABLE in self.attrs
@property
def is_optional(self):
return EncodableValueAttr.OPTIONAL in self.attrs
@property
def is_list(self):
return EncodableValueAttr.LIST in self.attrs
@property
def is_octet_string(self):
return self.data_type.name.lower() in ['octet_string', 'long_octet_string']
@property
def is_char_string(self):
return self.data_type.name.lower() in ['char_string', 'long_char_string']
@property
def is_struct(self):
return self.context.is_struct_type(self.data_type.name)
@property
def is_enum(self):
return self.context.is_enum_type(self.data_type.name)
@property
def is_bitmap(self):
return self.context.is_bitmap_type(self.data_type.name)
@property
def is_untyped_bitmap(self):
return self.context.is_untyped_bitmap_type(self.data_type.name)
def clone(self):
return EncodableValue(self.context, self.data_type, self.attrs)
def without_nullable(self):
result = self.clone()
result.attrs.remove(EncodableValueAttr.NULLABLE)
return result
def without_optional(self):
result = self.clone()
result.attrs.remove(EncodableValueAttr.OPTIONAL)
return result
def without_list(self):
result = self.clone()
result.attrs.remove(EncodableValueAttr.LIST)
return result
def get_underlying_struct(self):
s = self.context.find_struct(self.data_type.name)
if not s:
raise Exception("Struct %s not found" % self.data_type.name)
return s
def get_underlying_enum(self):
e = self.context.find_enum(self.data_type.name)
if not e:
raise Exception("Enum %s not found" % self.data_type.name)
return e
@property
def boxed_java_type(self):
t = ParseDataType(self.data_type, self.context)
if isinstance(t, FundamentalType):
if t == FundamentalType.BOOL:
return "Boolean"
elif t == FundamentalType.FLOAT:
return "Float"
elif t == FundamentalType.DOUBLE:
return "Double"
else:
raise Exception("Unknown fundamental type")
elif isinstance(t, BasicInteger):
# the >= 3 will include int24_t to be considered "long"
if t.byte_count >= 3:
return "Long"
else:
return "Integer"
elif isinstance(t, BasicString):
if t.is_binary:
return "byte[]"
else:
return "String"
elif isinstance(t, IdlEnumType):
if t.base_type.byte_count >= 3:
return "Long"
else:
return "Integer"
elif isinstance(t, IdlBitmapType):
if t.base_type.byte_count >= 3:
return "Long"
else:
return "Integer"
else:
return "Object"
@property
def boxed_java_signature(self):
# Optional takes precedence over list - Optional<ArrayList> compiles down to just java.util.Optional.
if self.is_optional:
return "Ljava/util/Optional;"
if self.is_list:
return "Ljava/util/ArrayList;"
t = ParseDataType(self.data_type, self.context)
if isinstance(t, FundamentalType):
if t == FundamentalType.BOOL:
return "Ljava/lang/Boolean;"
elif t == FundamentalType.FLOAT:
return "Ljava/lang/Float;"
elif t == FundamentalType.DOUBLE:
return "Ljava/lang/Double;"
else:
raise Exception("Unknown fundamental type")
elif isinstance(t, BasicInteger):
if t.byte_count >= 3:
return "Ljava/lang/Long;"
else:
return "Ljava/lang/Integer;"
elif isinstance(t, BasicString):
if t.is_binary:
return "[B"
else:
return "Ljava/lang/String;"
elif isinstance(t, IdlEnumType):
if t.base_type.byte_count >= 3:
return "Ljava/lang/Long;"
else:
return "Ljava/lang/Integer;"
elif isinstance(t, IdlBitmapType):
if t.base_type.byte_count >= 3:
return "Ljava/lang/Long;"
else:
return "Ljava/lang/Integer;"
else:
return "Lchip/devicecontroller/ChipStructs${}Cluster{};".format(self.context.cluster.name, self.data_type.name)
def EncodableValueFrom(field: Field, context: TypeLookupContext) -> EncodableValue:
"""
Filter to convert a standard field to an EncodableValue.
This converts the AST information (field name/info + lookup context) into
a java-generator specific wrapper that can be manipulated and
queried for properties like java native name or JNI string signature.
"""
attrs = set()
if field.is_optional:
attrs.add(EncodableValueAttr.OPTIONAL)
if field.is_nullable:
attrs.add(EncodableValueAttr.NULLABLE)
if field.is_list:
attrs.add(EncodableValueAttr.LIST)
return EncodableValue(context, field.data_type, attrs)
def CreateLookupContext(idl: Idl, cluster: Cluster) -> TypeLookupContext:
"""
A filter to mark a lookup context to be within a specific cluster.
This is used to specify how structure/enum/other names are looked up.
Generally one looks up within the specific cluster then if cluster does
not contain a definition, we loop at global namespacing.
"""
return TypeLookupContext(idl, cluster)
def CanGenerateSubscribe(attr: Attribute, lookup: TypeLookupContext) -> bool:
"""
Filter that returns if an attribute can be subscribed to.
Uses the given attribute and the lookupContext to figure out the attribute
type.
"""
# For backwards compatibility, we do not subscribe to structs
# (although list of structs is ok ...)
if attr.definition.is_list:
return True
return not lookup.is_struct_type(attr.definition.data_type.name)
def IsResponseStruct(s: Struct) -> bool:
return s.tag == StructTag.RESPONSE
class __JavaCodeGenerator(CodeGenerator):
"""
Code generation for java-specific files.
Registers filters used by all java generators.
"""
def __init__(self, storage: GeneratorStorage, idl: Idl, **kargs):
"""
Inintialization is specific for java generation and will add
filters as required by the java .jinja templates to function.
"""
super().__init__(storage, idl, fs_loader_searchpath=os.path.dirname(__file__))
self.jinja_env.filters['attributesWithCallback'] = attributesWithSupportedCallback
self.jinja_env.filters['callbackName'] = CallbackName
self.jinja_env.filters['chipClustersCallbackName'] = ChipClustersCallbackName
self.jinja_env.filters['delegatedCallbackName'] = DelegatedCallbackName
self.jinja_env.filters['commandCallbackName'] = CommandCallbackName
self.jinja_env.filters['named'] = NamedFilter
self.jinja_env.filters['toBoxedJavaType'] = ToBoxedJavaType
self.jinja_env.filters['lowercaseFirst'] = LowercaseFirst
self.jinja_env.filters['asEncodable'] = EncodableValueFrom
self.jinja_env.filters['createLookupContext'] = CreateLookupContext
self.jinja_env.filters['canGenerateSubscribe'] = CanGenerateSubscribe
self.jinja_env.filters['decodableJniType'] = DecodableJniType
self.jinja_env.tests['is_response_struct'] = IsResponseStruct
self.jinja_env.tests['is_using_global_callback'] = _IsUsingGlobalCallback
class JavaJNIGenerator(__JavaCodeGenerator):
"""Generates JNI java files (i.e. C++ source/headers)."""
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def internal_render_all(self):
"""
Renders .CPP files required for JNI support.
"""
large_targets = [
GenerateTarget(template="CHIPCallbackTypes.jinja",
output_name="jni/CHIPCallbackTypes.h"),
GenerateTarget(template="CHIPReadCallbacks_h.jinja",
output_name="jni/CHIPReadCallbacks.h")
]
for target in large_targets:
self.internal_render_one_output(
template_path=target.template,
output_file_name=target.output_name,
vars={
'idl': self.idl,
'clientClusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT],
'globalTypes': _GLOBAL_TYPES,
}
)
cluster_targets = [
GenerateTarget(template="ChipClustersRead.jinja",
output_name="jni/{cluster_name}Client-ReadImpl.cpp"),
GenerateTarget(template="ChipClustersCpp.jinja",
output_name="jni/{cluster_name}Client-InvokeSubscribeImpl.cpp"),
]
self.internal_render_one_output(
template_path="CHIPCallbackTypes.jinja",
output_file_name="jni/CHIPCallbackTypes.h",
vars={
'idl': self.idl,
'clientClusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT],
}
)
# Every cluster has its own impl, to avoid
# very large compilations (running out of RAM)
for cluster in self.idl.clusters:
if cluster.side != ClusterSide.CLIENT:
continue
for target in cluster_targets:
self.internal_render_one_output(
template_path=target.template,
output_file_name=target.output_name.format(
cluster_name=cluster.name),
vars={
'cluster': cluster,
'typeLookup': TypeLookupContext(self.idl, cluster),
'globalTypes': _GLOBAL_TYPES,
}
)
class JavaClassGenerator(__JavaCodeGenerator):
"""Generates .java files """
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def internal_render_all(self):
"""
Renders .java files required for java matter support
"""
clientClusters = [
c for c in self.idl.clusters if c.side == ClusterSide.CLIENT]
self.internal_render_one_output(
template_path="ClusterReadMapping.jinja",
output_file_name="java/chip/devicecontroller/ClusterReadMapping.java",
vars={
'idl': self.idl,
'clientClusters': clientClusters,
}
)
self.internal_render_one_output(
template_path="ClusterWriteMapping.jinja",
output_file_name="java/chip/devicecontroller/ClusterWriteMapping.java",
vars={
'idl': self.idl,
'clientClusters': clientClusters,
}
)
self.internal_render_one_output(
template_path="ClusterIDMapping.jinja",
output_file_name="java/chip/devicecontroller/ClusterIDMapping.java",
vars={
'idl': self.idl,
'clientClusters': clientClusters,
}
)