| #!/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. |
| |
| from idl.generators import CodeGenerator, GeneratorStorage |
| from idl.matter_idl_types import Idl, ClusterSide, Field, Attribute, Cluster, FieldQuality, Command, DataType |
| from idl import matter_idl_types |
| from idl.generators.types import ParseDataType, BasicString, BasicInteger, FundamentalType, IdlType, IdlEnumType, IdlBitmapType, TypeLookupContext |
| from typing import Union, List, Set |
| from stringcase import capitalcase |
| |
| import enum |
| import logging |
| |
| |
| def FieldToGlobalName(field: Field, context: TypeLookupContext) -> Union[str, None]: |
| """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 |
| |
| if FieldQuality.OPTIONAL & field.qualities: |
| return None |
| |
| actual = ParseDataType(field.data_type, context) |
| if type(actual) == IdlEnumType: |
| actual = actual.base_type |
| elif type(actual) == IdlBitmapType: |
| actual = actual.base_type |
| |
| if type(actual) == BasicString: |
| if actual.is_binary: |
| return 'OctetString' |
| else: |
| return 'CharString' |
| elif type(actual) == BasicInteger: |
| # TODO: unclear why this, but tries to match zap: |
| if actual.idl_name.lower() in ['vendor_id', 'fabric_idx']: |
| return None |
| |
| if actual.is_signed: |
| return "Int{}s".format(actual.power_of_two_bits) |
| else: |
| return "Int{}u".format(actual.power_of_two_bits) |
| elif type(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 CallbackName(attr: Attribute, cluster: Cluster, 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(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 type(underlying) == IdlType: |
| continue |
| |
| yield attr |
| |
| |
| 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): |
| self.context.is_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 type(t) == FundamentalType: |
| if t == FundamentalType.BOOL: |
| return "Boolean" |
| elif t == FundamentalType.FLOAT: |
| return "Float" |
| elif t == FundamentalType.DOUBLE: |
| return "Double" |
| else: |
| raise Error("Unknown fundamental type") |
| elif type(t) == BasicInteger: |
| if t.byte_count >= 4: |
| return "Long" |
| else: |
| return "Integer" |
| elif type(t) == BasicString: |
| if t.is_binary: |
| return "byte[]" |
| else: |
| return "String" |
| elif type(t) == IdlEnumType: |
| return "Integer" |
| elif type(t) == IdlBitmapType: |
| 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 type(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 Error("Unknown fundamental type") |
| elif type(t) == BasicInteger: |
| if t.byte_count >= 4: |
| return "Ljava/lang/Long;" |
| else: |
| return "Ljava/lang/Integer;" |
| elif type(t) == BasicString: |
| if t.is_binary: |
| return "[B" |
| else: |
| return "Ljava/lang/String;" |
| elif type(t) == IdlEnumType: |
| return "Ljava/lang/Integer;" |
| elif type(t) == IdlBitmapType: |
| 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) |
| |
| |
| class JavaGenerator(CodeGenerator): |
| """ |
| Generation of java code for matter. |
| """ |
| |
| def __init__(self, storage: GeneratorStorage, idl: Idl): |
| """ |
| Inintialization is specific for java generation and will add |
| filters as required by the java .jinja templates to function. |
| """ |
| super().__init__(storage, idl) |
| |
| self.jinja_env.filters['attributesWithCallback'] = attributesWithSupportedCallback |
| self.jinja_env.filters['callbackName'] = CallbackName |
| 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 |
| |
| def internal_render_all(self): |
| """ |
| Renders .CPP files required for JNI support. |
| """ |
| # 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 |
| |
| self.internal_render_one_output( |
| template_path="java/ChipClustersRead.jinja", |
| output_file_name="jni/%sClient-ReadImpl.cpp" % cluster.name, |
| vars={ |
| 'cluster': cluster, |
| 'typeLookup': TypeLookupContext(self.idl, cluster), |
| } |
| ) |
| |
| self.internal_render_one_output( |
| template_path="java/ChipClustersCpp.jinja", |
| output_file_name="jni/%sClient-InvokeSubscribeImpl.cpp" % cluster.name, |
| vars={ |
| 'cluster': cluster, |
| 'typeLookup': TypeLookupContext(self.idl, cluster), |
| } |
| ) |