Annotations
Cangjie provides some attribute macros for special case handling.
Annotations for Ensuring the Correct Use of Integer Operation Overflow Policy
Cangjie provides three types of attribute macros to specify the handling policy of integer overflow: @OverflowThrowing
, @OverflowWrapping
, and @OverflowSaturating
. These attribute macros can only be labeled on function declarations and are used for integer operations and integer conversion in functions. They correspond to the following three overflow handling policies:
(1) Throwing: An exception is thrown when the integer operation overflows.
@OverflowThrowing
main() {
let res: Int8 = Int8(100) + Int8(29)
/* 100 + 29 equals to 129 in mathematics.
* An overflow occurs within the representation range of Int8.
* The program throws an exception.
*/
let con: UInt8 = UInt8(-132)
/* -132 causes an underflow within the representation range of UInt8.
* The program throws an exception.
*/
0
}
(2) Wrapping: When the result of an integer operation falls outside the data range that can be represented by the memory space used to receive the result, the part that exceeds the memory space is truncated.
@OverflowWrapping
main() {
let res: Int8 = Int8(105) * Int8(4)
/* 105 * 4 equals to 420 in mathematics.
* The value is 1 1010 0100 in binary.
* The 8-bit memory space used to receive the result is exceeded.
* The truncated result is represented as 1010 0100 in binary.
* The value equals to the signed integer -92.
*/
let temp: Int16 = Int16(-132)
let con: UInt8 = UInt8(temp)
/* -132 equals to 1111 1111 0111 1100 in binary.
* The 8-bit memory space used to receive the result is exceeded.
* The truncated result is represented as 0111 1100 in binary.
* The value equals to the signed integer 124.
*/
0
}
(3) Saturating: When an integer operation overflows, the result is set to the maximum or minimum value in the corresponding fixed precision.
@OverflowSaturating
main() {
let res: Int8 = Int8(-100) - Int8(45)
/* -100 - 45 equals to -145 in mathematics.
* An underflow occurs within the representation range of Int8.
* Set the result to the minimum Int8 value -128.
*/
let con: Int8 = Int8(1024)
/* 1024 causes an overflows within the representation range of Int8.
* Set the result to the maximum Int8 value 127.
*/
0
}
By default (that is, when the attribute macro is not labeled), the handling policy of the @OverflowThrowing
exception is used.
In practice, you need to select a proper overflow policy based on service scenario requirements. For example, to implement a secure operation on Int32
so that the calculation result is mathematically equal to the calculation process, you can use the exception throwing policy.
[Negative Example]
// The high-order bits of the calculation result are truncated.
@OverflowWrapping
func operation(a: Int32, b: Int32): Int32 {
a + b // No exception will be thrown when overflow occurs
}
The negative example uses the high-order-bit truncation policy. For example, when two large input parameters a
and b
cause a result overflow, it leads to a high-order-bit truncation. Consequently, the result returned by the function does not mathematically equal to the calculation expression a + b
.
[Positive Example]
// Secure
@OverflowThrowing
func operation(a: Int32, b: Int32): Int32 {
a + b
}
main() {
try {
operation(a, b)
} catch (e: ArithmeticException) {
//Handle error
}
0
}
The positive example correctly uses the Throwing overflow policy. When two large input parameters a
and b
cause an integer overflow, the operation
function throws an exception.
The following summarizes mathematical operators that may cause an integer overflow.
Operator | Overflow | Operator | Overflow | Operator | Overflow | Operator | Overflow |
---|---|---|---|---|---|---|---|
+ | Y | -= | Y | << | N | < | N |
- | Y | *= | Y | >> | N | > | N |
* | Y | /= | Y | & | N | >= | N |
/ | Y | %= | N | | | N | <= | N |
% | N | <<= | N | ^ | N | == | N |
++ | Y | >>= | N | **= | Y | ||
-- | Y | &= | N | ! | N | ||
= | N | |= | N | != | N | ||
+= | Y | ^= | N | ** | Y |
Performance Optimization Annotations
To facilitate the interoperability with the C
language, Cangjie provides the @FastNative
attribute macro to specify the calling of the C
function through backend cjnative
optimization. It is worth noting that the @FastNative
attribute macro can only be used for functions declared by foreign
.
Constraints on @FastNative
When using @FastNative
to modify the foreign
function, ensure that the corresponding C
function meets the following requirements:
- First, it is recommended that the overall execution time of the function be short. For example:
- A large loop is not allowed in the function.
- Function blocking is not allowed when a function such as
sleep
orwait
is called.
- Second, Cangjie methods cannot be called in a function.
Custom Annotations
The custom annotation mechanism enables reflection (see the Reflection section) to obtain the annotation content. This mechanism is tailor-made for providing more useful information apart from the type metadata to support more complex logic.
You can customize annotations through @Annotation
. @Annotation
can only modify a class
that is not modified by abstract
, open
, or sealed
. When a class
declares that it is labeled with @Annotation
, it must provide at least one const init
function. Otherwise, the compiler reports an error.
The following example defines a custom annotation @Version
and uses it to modify A
, B
and C
. In main
, the @Version
annotations on the classes are obtained through reflections and printed.
package pkg
import std.reflect.TypeInfo
@Annotation
public class Version {
let code: String
const init(code: String) {
this.code = code
}
}
@Version["1.0"]
class A {}
@Version["1.1"]
class B {}
main() {
let objects = [A(), B()]
for (obj in objects) {
let annOpt = TypeInfo.of(obj).findAnnotation<Version>()
if (let Some(ann) <- annOpt) {
if (let Some(version) <- ann as Version) {
println(version.code)
}
}
}
}
The preceding code is compiled and executed, and outputs the following:
1.0
1.1
Annotations need to be generated during compilation and bound to classes. const init
must be used in annotation customization to construct valid instances. The declaration syntax of an annotation is the same as that of a macro. Parameters in []
must be passed by sequence or naming rule and must be const expressions. For details, see the constant evaluation section. Annotation classes with non-parameterized constructors can omit brackets in declaration.
The following example defines a custom annotation @Deprecated
with a non-parameterized constructor const init
. Therefore, both @Deprecated
or @Deprecated[]
are acceptable.
package pkg
import std.reflect.TypeInfo
@Annotation
public class Deprecated {
const init() {}
}
@Deprecated
class A {}
@Deprecated[]
class B {}
main() {
if (TypeInfo.of(A()).findAnnotation<Deprecated>().isSome()) {
println("A is deprecated")
}
if (TypeInfo.of(B()).findAnnotation<Deprecated>().isSome()) {
println("B is deprecated")
}
}
The preceding code is compiled and executed, and outputs the following:
A is deprecated
B is deprecated
An annotation class cannot be declared for multiple times for an annotation target. That is, the annotation class must be unique.
@Deprecated
@Deprecated // Error
class A {}
Annotation
is not inherited. Therefore, the annotation metadata of a class comes only from the declared annotations when it is defined. If the annotation metadata of the parent class is required, you need to query the metadata through the reflection interface.
In the following example, A
is modified by the @Deprecated
annotation. B
inherits A
but does not obtain the annotation of A
.
package pkg
import std.reflect.TypeInfo
@Annotation
public class Deprecated {
const init() {}
}
@Deprecated
open class A {}
class B <: A {}
main() {
if (TypeInfo.of(A()).findAnnotation<Deprecated>().isSome()) {
println("A is deprecated")
}
if (TypeInfo.of(B()).findAnnotation<Deprecated>().isSome()) {
println("B is deprecated")
}
}
The preceding code is compiled and executed, and outputs the following:
A is deprecated
Custom annotations can be used in type declarations (class
, struct
, enum
, and interface
), parameters in member functions or constructors, constructor declarations, member function declarations, member variable declarations, and member attribute declarations. They can also restrict their application scopes to reduce misuse. To this aim, you need to add the target
parameter of the Array<AnnotationKind>
type when declaring @Annotation
. Where, AnnotationKind
is an enum
defined in the standard library. If no target is specified, the custom annotations can be used in all the preceding scopes. When the target is specified, it can be used only in the declared list.
public enum AnnotaitionKind {
| Type
| Parameter
| Init
| MemberProperty
| MemberFunction
| MemberVariable
}
The following example limits the use of custom annotations to member functions through target
. Otherwise, a compilation error is reported.
@Annotation[target: [MemberFunction]]
public class Deprecated {
const init() {}
}
class A {
@Deprecated // Ok, member funciton
func deprecated() {}
}
@Deprecated // Error, type
class B {}
main() {}