Original address: github.com/google/refl…
Original author:
Release Date:
The purpose of this paper is to conceptually introduced our design choices for ReflectCapability class and its subtypes, the class is in the package of reflectable package: reflectable/capability. The dart in the statement of a set of classes. For more programming insights, consult the library’s documentation. We use the word capability to specify instances of the subtype of the ReflectCapability class. This class and its subtypes are used when specifying how well the client of package Reflectable supports reflection operations in a given context, for example, for instances of a particular class. We use the term client when referring to libraries that import and use the package reflectable or packages that contain such a library. Using one capability or another as metadata for class C in client code can determine whether a method can be called reflexively on an instance of C via InstanceMirror. Since one of the main reasons to make a package reflective in the first place is to save space consumed by less frugal reflection types, the ability to limit reflection support to actual requirements is a core point in package design.
It should be noted that the concept of capability in this article and in relation to reflective packets in general is different from the concept of capability known in operating system research, which relates to unforgeable authorization tokens (that is, large, dense numbers). Here, capability is concerned only with the ability to do something, not with security.
Background and design ideas
To understand the topics covered in this article, we need a brief overview of how to understand reflective packages as a whole. We then go on to explain how we divide up the universes that support possible categories of reflection so that we have a set of categories of reflection to choose from. Finally, we explain how capabilities can be used to select among these options and how they can be applied to specific parts of the client program.
Reflective package
The package reflectable is an example of support for mirror-based introspective reflection in general object-oriented languages, and it should be understood as such [1]. More specifically, the reflection API provided by the package Reflectable was copied verbatim from the API provided by the package Dart :mirrors and modified in some way. Therefore, the code using Dart: Mirrors should be very similar to the corresponding code using the Reflectable package. The difference does exist for two reasons.
-
By design, some operations declared as top-level functions in Dart :mirrors are declared as methods of the Reflectable class in package reflectable, because instances of its subclasses are called reflectors and are intended to play the role of mirroring systems [1, or search for ‘mirror systems’ below], These operations are unique to the mirror system. For example, the top-level function reflect in Dart :mirrors corresponds to two different methods on two different mirror systems (semantically different, so they cannot be combined).
-
Some suggestions have been made to modify the Dart: Mirrors API. We took the opportunity to try to update the API by changing the signature of some methods. For example, InstanceMirror. Invoke returns the result of a method call, not the InstanceMirror that wraps it. In general, in cases where the mirror is often immediately discarded, the mirror operation returns the base-level value, not its mirror, and it is easy to create a mirror if needed. The method signature of the mirror class has also been modified in one way. When the DART :mirrors method accepts arguments or returns results involving symbols, the Package Reflectable uses String. This helps avoid the difficulties associated with minification (which is an automatic, pervasive renaming procedure that is applied to programs primarily to save space), since String values remain constant throughout compilation.
-
Some mirrors add new methods so that package Reflectable also provides an extended API compared to Dart :mirrors. In particular, variable and parameter mirrors support the reflectedType method, and method mirrors support reflectedReturnType. These methods is the short hand method invocation chain (that is to say, their works are respectively. The reflectedType. ReflectedType). The reason for having them is that intermediate class mirrors do not need to exist, which means that in the case of a large number of class mirrors, space consumption can be reduced because they appear as intermediate results when executing those particular method call chains. An added method on reflectors is getInstance, which returns a typical reflector for a given reflector class; This method is needed to implement meta-level reflection, which programmatically scans the current program to find the appropriate mirroring system.
In summary, most of the apis provided by the package reflectable are the same as those provided by dart:mirrors, and the design documentation [2,3] for this API or general reflection will be used to document basic ideas and design choices.
Reflective capability design
The obvious new element in the package Reflectable is that it allows clients to specify support for reflection in a new way by using capabilities in metadata. This section Outlines the semantics of reflective capabilities, that is, what standards they should be able to express.
In general, we maintain the property that a reflection support specification with one reflector (that is, within a mirror system) is monotonous, that is, any program with a certain amount of reflection support will support at least as many reflection operations if additional specifications are added to a given reflector. In other words, reflection support specifications can request additional functionality, and they can never prevent any reflection functionality from being supported. As a result, we have a modularity law: if a programmer browses the source code and encounters the reflection support specification S somewhere, he can always trust that there is some kind of reflection support in the program. Other parts of the program can still add more reflection support, but they cannot withdraw the functionality required by S. Similarly, specifications are equivalent, that is, it is harmless for multiple specifications to require the same functionality or overlapping sets of functionality, and it makes no difference if a particular thing is required once or more.
Capabilities based on mirroring apis
In principle, the level of support for reflection can be specified in many ways: there are plenty of ways to use reflection, and ideally, clients should be able to request exactly the support they need. To greatly simplify this possibility, and to maintain a useful level of expressiveness, we decided to use the following layers as an overall framework for the design.
-
The most basic reflection support specification deals directly with the API of the mirror class, that is, it is concerned with “turning on” support for the use of individual methods or small groups of methods in the mirror class. For example, you could use one ability to turn on support for Instancemiror.invoke and another to turn on classMirror.invoke. If a supported method is called, it behaves just like the corresponding method in the corresponding mirror class from Dart: Mirrors (except for the adjustments mentioned above, such as returning a base value instead of its mirror). If an unsupported method is called, an exception is thrown.
-
As a refinement of the API-based specification, we chose to focus on the specification of allowable parameter values for specific methods in the API. For example, we can specify a predicate to filter existing method names, so instancemiror.invoke is supported because its name matches this predicate. An example of this is that in tests, all names are given with… It might be a convenient feature to call the methods at the end of Test reflectively, rather than purely static methods, where someone has to write a centralized list of all such methods that can then be called.
With these mechanisms, it is possible to specify support for reflection in terms of mirrors and the functionality they provide, independent of the actual source code in the client program.
Reflex-based ability
Another dimension that supports reflection is selecting which parts of the client can be reflected, including when ClassMirror is reflected into one of the classes, and when InstanceMirror is reflected into one of the instances. In short, this dimension is about the choice of available reflectors.
A common feature of specifications covering this type is quantification of source code elements, particularly classes and other top-level declarations. In this area we focus on the mechanisms listed below. Note that MyReflector is assumed to be the name of a subclass of Reflectable, and MyReflector is assumed to be a const instance of MyReflector, its only const instance through normalization. This allows us to use the example myReflector to refer to the general concept of reflector, its classes and similar related declarations.
-
Reflection support starts by calling a method in Reflector reflectType on myReflector. We chose to omit the ability to do reflection (which is always possible in this sense) because there is no reason to have reflection if mirroring of the instance is not supported. Instead, we chose the ability to get class mirrors and similar source-oriented mirrors, which also controls the ability to execute reflectType; This is because having these mirrors can be expensive in terms of program size, and in some cases may not be necessary. Finally, we chose to omit the reflectClass method because it might be replaced by reflectType, followed by originalDeclaration when isOriginalDeclaration is false.
-
The basic mechanism for getting reflection support for class C is to attach metadata to it, and that metadata must be a reflector, such as myReflector. The class reflectables has a constructor, which is const and takes a single parameter of type List
, and a constructor, It accepts up to 10 parameters of type ReflectCapability (thus avoiding a template that explicitly makes it a list). MyReflector must have a single constructor, which is constant and accepts zero arguments. Therefore, it is mandatory for MyReflector to pass List
through a hyperinitializer in its constructor so that every instance of MyReflector has the same state, “same capability.” In summary, this basic mechanism requests reflection support for a class, the level of which is specified by the capability stored in the metadata.
-
The reflection support specification can be nonlocal, that is, it can be placed in a different place in the program than the target class itself. This is required when reflection support needs to be requested for a class in the library that cannot be edited (it may be predefined, it may be provided by a third party, so that changes after updates cause repeated maintenance, and so on). This feature has been called side labels since the beginning of reflective packs. They must be as metadata attached to the library package: reflectable/reflectable dart import instructions.
-
Quantization generalizes the single-class specification, allowing a single specification to specify as its parameters the ability to apply to a set of classes or other program elements. It’s easy to provide quantization mechanisms, but we don’t want to contaminate the package with confusingly rich quantization mechanisms, so each of our quantization mechanisms should be understandable, reasonable, and powerful, and they shouldn’t overlap. So far, we’ve focused on the following variants.
- It should be possible to request reflection support for a selected set of classes through some query mechanism. The obvious candidate quantization mechanism quantifies all superclasses; All supertypes; All subclasses; All subtypes of a given class; And all classes whose names match the given pattern.
- The aforementioned quantization is centralized because it is based on a specification that is then used to “query” matching entities throughout the program. It is common and useful to complement this with a decentralized mechanism whereby programmers explicitly tag each member of a collection, for example, by attaching a tag as metadata to those members. This makes it possible to maintain collections precisely and unambiguously, even in cases where members have no obvious common characteristics that make a centralized “query” approach unsuitable. A good example is a set of methods that can be reflexive supported by annotating them with metadata; For example, we might want to be able to reflexively call all methods marked @businessRule.
It is worth noting the flexibility resulting from the separation of mechanisms that support specific methods on mirrors (API dependent) and support specific target classes (reflectee dependent). This separation is due to the fact that API-dependent support is specified in the reflector class by capability, whereas reflector dependent support is specified by adding reflectors as metadata to top-level declarations such as classes, and by global quantifiers. In particular, we can use a reflector declared in some third-party package and then specify locally which classes the reflector should provide reflection support for, since there is no need to edit the reflector class itself.
We subscribe to the view that reflection operations are divided into (a) operations related to the dynamic behavior of instances and (b) operations related to the structure of programs; Let’s call the former a behavioral operation and the latter an introspective operation. For example, using Instancemiror.invoke to execute a reflected class’s method is a behavioral operation, while using classMirror.declarations to investigate the set of members an instance of a reflected class will have is an introspective operation.
An important consequence of this distinction is that behavioral operations are concerned with the actual behavior of objects, meaning that inherited method implementations have the same status as method implementations declared in classes, which are the runtime types of those being reflected. Instead, introspection focuses on source code entities such as declarations, so the declarations reported by a given class do not include inherited declarations, which must be found by explicitly iterating through the superclass chain. Similarly, introspective perspectives include abstract member declarations, but they are ignored when behavioral perspectives are used.
Finally, we need to elaborate a little bit on the concept of mirror systems, a term we’ve used a few times. As mentioned earlier, Bracha and Ungar established the conceptual basis of mirroring and mirroring systems in the 2004 OOPSLA paper [1]. Mirror system is a set of functions that it in a special environment based on mirror reflection provides support, for example, for certain class or in the implementation of the specific method, instead of all the classes and all methods, or mirror can only provide some functions, such as, for instance methods of reflective calls, rather than for a static method. Typical examples could also be mirrors customized for remote debugging, or for compile-time reflection, but these examples are less relevant here.
For the package Reflectable, we need the concept of a mirror system because it is useful to have several different mirror systems in the same application, for example, when a few classes require extensive reflection support and a large number of other classes require little. In this case, using a powerful mirroring system in the former and a minimal mirroring system in the latter may be worth the effort due to the increased economy of resources on a global scale.
Some additional complexity must be anticipated; For example, if we can take the same object at the same time to obtain a “cheap” and a “strong” image, this will be a similar myCheapReflectable. Reflect (o) is respectively myPowerfulReflectable. Reflect (o). It’s up to the programmer to avoid asking cheap objects to do powerful things. In return, the entire program may save a lot of space, compared with a single mirrored system where each class that needs reflection must carry the full set of data to perform the most demanding reflection anywhere in the program.
Specifies the ability to reflect
As this article mentioned in the first quarter, reflection ability is rooted in the package: reflectable/capability. The dart ReflectCapability subtypes of a class hierarchy in the specified. Instances of these classes are used to build something that might be thought of as abstract syntactic trees for domain-specific languages. This section describes how to use this setting to specify reflection support using the DOMain-specific language.
The subtype hierarchy under ReflectCapability is sealed, in the sense that there is a set of subtypes of ReflectCapability in the library, and there should never be any other subtypes of that class, as described below.
As constant values, instances of these classes obviously cannot have mutable state, but some of them do contain constant values, such as strings or types. Capabilities have no methods, except those they inherit from Object. In general, this means that instances of these classes can’t “do anything,” but they can be used to build immutable trees, and the range of possible trees is fixed because the collection of classes is fixed. This makes these trees similar to abstract syntax trees, to which we can assign a semantics from the outside. This semantics can be implemented by an interpreter or translator. The tightness of the set of classes involved is required because the unknown subtypes of ReflectCapability will have no semantics and the interpreter and translator will not be able to handle them.
In other words, we specify reflection capabilities by establishing a representation of an expression in a domain-specific language; We call this language reflexive language. The language has a translator, which is an integrated part of implementing reflective packages, or code generators.
Obviously, there are many ways to express things in this language, and we considered introducing a traditional, textual syntax to it. We could have a parser that takes a String, parses it, and produces an abstract syntax tree consisting of instances of a subtype of ReflectCapability, or reports a syntax error. You can provide a Reflectable constructor that takes a String argument and can parse the String if needed. This would be a convenient (but less secure) way for programmers to specify reflection support as an alternative to the current way in which abstract syntax trees must be specified directly.
However, text syntax is used in this document only because it is concise and readable, and it has not (and probably never will) be implemented. Therefore, the actual code in a reflective language would have to use a more crude form, that is, directly build an object structure that represents the abstract syntax tree of the expression. Sample code showing how to do this can be found in the package test_reflectable.
In this article, we will discuss the language in terms of syntactic structures, and the informal semantics of each structure.
Specifies mirroring API-based capabilities
Figure 1 shows the raw material for some of the elements of the reflective language syntax. The left side of the diagram contains tags that represent the cluster’s abstract concepts, and the right side contains tags that represent each method in the entire mirroring API. There are several tags that represent more than one method (for example, all VariableMirrors, MethodMirrors, and TypeVariableMirrors have an isStatic getter, and the metadata is also defined in both classes), but they are combined into one tag, Because these methods play the same semantic role in all the contexts in which they appear. In other semantically different cases (invoke, invokeGetter, invokeSetter, and declarations), each method name has multiple tags, with an _ ending prefix to denote the enclosing mirror class.
Strong | Specialization |
---|---|
invocation | instance_invoke | class_invoke | library_invoke | instance_invokeGetter | class_invokeGetter | library_invokeGetter | instance_invokeSetter | class_invokeSetter | library_invokeSetter | delegate | apply | newInstance |
naming | simpleName | qualifiedName | constructorName |
classification | isPrivate | isTopLevel | isImport | isExport | isDeferred | isShow | isHide | isOriginalDeclaration | isAbstract | isStatic | isSynthetic | isRegularMethod | isOperator | isGetter | isSetter | isConstructor | isConstConstructor | isGenerativeConstructor | isRedirectingConstructor | isFactoryConstructor | isFinal | isConst | isOptional | isNamed | hasDefaultValue | hasReflectee | hasReflectedType |
annotation | metadata |
typing | instance_type | variable_type | parameter_type | typeVariables | typeArguments | originalDeclaration | isSubtypeOf | isAssignableTo | superclass | superinterfaces | mixin | isSubclassOf | returnType | upperBound | referent |
concretization | reflectee | reflectedType |
introspection | owner | function | uri | library_declarations | class_declarations | libraryDependencies | sourceLibrary | targetLibrary | prefix | combinators | instanceMembers | staticMembers | parameters | callMethod | defaultValue |
text | location | source |
Figure 1. Raw material for the reflective capability language API.
Figure 2 shows reducing this raw material to a set of capabilities that we think make sense. It doesn’t allow programmers to choose their capabilities with the same degree of detail, but we hope that the reduction in complexity is worth enough to justify less fine-grained control.
We added the re parameter, specifying that each of these capabilities can meaningfully apply pattern matching constraints to select included methods, getters, and so on. Specifically, this parameter is a string used as a regular expression. An empty regular expression is the default, which means that when the regular expression is omitted, all entities in the related category are included.
Similarly, we create variations that have a MetadataClass parameter indicating that the entity in the related category, if annotated with metadata, is of type a subtype of the given MetadataClass (which can be a trivial subtype, i.e., MetadataClass itself), Then the entity is included. This parameter is an instance of Type Type corresponding to the predefined class.
Overall, this provides support for centralized and slightly abstract entity selection using regular expressions, and it provides support for decentralized entity selection using metadata to explicitly tag entities.
It is important to note that MetadataClass may not be related to the package reflectable. We have a use case where a class C from a reflectable unrelated package P is just right because instances of C are already attached as metadata to the associated member collection. This may be because some other packages require C metadata for other purposes related to reflection requirements, such as serialization.
Non-terminal | Expansion |
---|---|
apiSelection | invocation | annotation | typing | introspection |
invocation | instanceInvoke([ RegExp ]) | instanceInvokeMeta( MetadataClass ) | staticInvoke([ RegExp ]) | staticInvokeMeta( MetadataClass ) | topLevelInvoke([ RegExp ]) | topLevelInvokeMeta( MetadataClass ) | newInstance([ RegExp ]) | newInstanceMeta( MetadataClass ) |
delegation | delegate |
annotation | metadata |
typing | type | typeRelations |
introspection | owner | declarations | uri | libraryDependencies |
Figure 2. Syntax markup for the reflective language API.
In the class call, we use the prefix topLevel instead of library, because this term is common in the documentation of existing mirror classes. Removing category naming and always providing support for functionality that has never been disabled in practice and is cheap to support; Categorization is handled the same way, as is reification. The category text was removed because we currently do not intend to support reflective access to the source code as a whole.
We left out apply and function because we don’t have support for ClosureMirror, and we don’t expect it anytime soon.
The class delegate is separated from the invocation because delegate support is quite expensive.
Typing of categories was simplified in several ways: instance_type was renamed type because it stood out. The reflectType method on the reflector is supported only if this capability exists. The capabilities of Variable_type, parameter_type, and returnType are unified as Type because they focus on the lookup of homogeneous mirrors, but the supported set of classes is controlled by type annotation quantifiers, as described below. To have some control over the level of detail of type-dependent mirroring, TypeVariables, typeArguments, originalDeclaration, isSubtypeOf, isAssignableTo, Superclass, Superinterfaces, mixin, isSubclassOf , upperBound, and Referent were unified into typeRelations; They all deal with the relationships between types, type variables, and type definitions, and can result in significant space retention if the related information is never used.
Category introspection has also been simplified. We unify class_declarations, library_declarations, instanceMembers, staticMembers, callMethod, Parameters, and defaultValue as declarations. Finally, we unify the import and export properties into libraryDependencies so that it contains the sourceLibrary, targetLibrary, prefix, and combinator. We retain the owner’s ability separately because we want the ability to find closed declarations for a given declaration to be too expensive to include implicitly as part of another capability. We also kept the URI functionality separate, because in some cases it was considered a security issue to save URI information in JavaScript translated code (needed to implement URI methods on library images).
Note that some reflective methods are non-basic, because they can be implemented entirely based on other reflective methods, the basic methods. This affects the capabilities isSubtypeOf, isAssignableTo, isSubclassOf, instanceMembers, and staticMembers. These methods can be implemented in a generic way, so they are provided as part of the package Reflectable rather than being generated. Therefore, they are supported if and only if the methods on which they depend are supported. This is what we mean when we say instanceMembers has been “unified” into the statement.
Succinctly covering multiple API-based capabilities
To avoid overly verbose syntax in cases where relatively extensive reflection support is required, we have chosen to introduce some grouping tags. They don’t contribute anything new, they just provide a more concise notation for some of the ability choices that are expected to come up a lot. Figure 3 shows these grouping tags. To help us remember the meaning of this additional grammar, we use words ending in “ing “to remind us of the tiny amount of abstraction involved in grouping several abilities into a structure.
Group | Meaning |
---|---|
invoking([ RegExp ]) |
instanceInvoke([ RegExp ]) .staticInvoke([ RegExp ]) .newInstance([ RegExp ]) |
invokingMeta( MetadataClass ) |
instanceInvokeMeta( MetadataClass ) .staticInvokeMeta( MetadataClass ) .newInstanceMeta( MetadataClass >) |
typing |
type .name .classify .metadata .typeRelations .owner .declarations .uri .libraryDependencies |
Figure 3. Grouping tokens for reflective languages.
Includes the semantics of the capability invoking(RegExp), where RegExp represents a given parameter and is the same as the semantics that include all three capabilities in the same row to the right of the figure, giving them the same RegExp as a parameter. Similarly, a callback () request with no arguments supports reflective calls to all instance methods, all static methods, and all constructors. The semantics of including capabilities invokingMeta(MetadataClass) are the same as those of including all three capabilities in the same line, with the same parameters. Finally, including the semantics of typing is all the ability to request support on the right side; That is, the request supports every characteristic relevant to the program structure information.
Automatically acquire relevant abilities
We chose to use a subtype structure between capabilities to ensure automatic relationships between some of them. For example, if you specify declarative capabilities, then type capabilities are automatically included. The reason for this is that it is useless to declare capabilities unless they are available in a mirror of some class or library, that is, there is no case where anyone needs to declare capabilities without requiring type capabilities. The details of this mechanism can be examined by examining the actual subtype relationships between capability classes. If one capability class C1 is a subtype of another capability class C0, then including C1 implies including C0.
Specifies reflector-based capabilities
In the previous section, we found a way to specify mirroring API-based capabilities as a syntax. It is very simple because it consists only of terminals, except that some of them require a parameter that restricts the supported parameters to matching names. As shown in Figure 2, the non-terminal apiSelection covers all of these terminals. We’ll use several of them at once, so a typical use would be a list, written apiSelection*.
In this section, we discuss how to request reflection support specified by a given apiSelection* for a particular set of program elements. The program element that receives reflection support is called the target of the specification, which itself is given as a hyperinitializer in a subclass of the Reflectable class (called MyReflector), with a unique instance (called MyReflector). Now, myReflector is used as metadata somewhere in the program, and each capability is only applicable as an annotation somewhere, which is discussed below.
Figure 4 shows how capabilities and annotations are constructed, typically starting with apiSelection*. The non-endpoints in this part of the syntax are named after the predetermined location of the metadata and are or contain the corresponding class of capabilities.
Non-terminals | Expansions |
---|---|
reflector | Reflectable( targetMetadata ) |
targetMetadata | apiSelection | subtypeQuantify | superclassQuantify( upperBound . excludeUpperBound ) | typeAnnotationQuantify( transitive ) | correspondingSetterQuantify | admitSubtype |
globalMetadata | globalQuantify( RegExp . reflector ) | globalQuantifyMeta( MetadataClass . reflector ) |
Figure 4. Reflected ability language target selection.
In practice, a reflector is an instance of a subclass of the Reflectable class that is attached directly to a class as metadata or passed to a global quantifier; In running example terms, it is the myReflector object. The reflector has a state that we model with targetMetadata. In the syntax of Figure 4, we use the identifier Reflectable to represent all subclasses, and we simulate state by having it accept the corresponding targetMetadata as an argument. The semantics of annotating a class with a given reflector depend on targetMetadata, as described below.
The targetMetadata capability can be a base-level capability set, known as apiSelection*, or it can be a quantifier that may take a parameter to express variants. The semantics of attaching a reflector containing a plain apiSelection to the target class C is to provide the level of reflection support for class C and instances specified by the given apiSelection.
The semantics of attaching a reflector containing subtypeQuantify to class C is that the reflection support specified by the apiSelection element given to the same reflector is provided to all classes that are subtypes of class C, including C itself as well as instances.
The semantics of attaching a reflector to class C with superclassQuantify(upperBound, excludeUpperBound) is, Reflection support specified by the apiSelection element for the same reflector is provided to all classes (and instances of them) that are superclasses of class C, including C itself, and stops at the given upperBound, or below it if excludeUpperBound is true. If excludeUpperBound is omitted, it is considered false, and if upperBound is omitted, it is considered Object.
According to these rules, the set of classes specified by a given reflector to receive reflection support is calculated as the minimum fixed point. For example, subtypeQuantify repeatedly adds direct subtypes of classes it already includes until it reaches a point where no classes are added. Fixed-point calculation adds subtypes first in one phase, then superclasses in the second phase. Note that if we use the reverse order, or run fixed-point iterations on both, then we trivially include all classes (under the upper bound, and the default upper bound: all classes) in the case of both quantifiers, so the chosen sort is the only sort that makes sense.
If declarative capabilities are specified, it is possible to get a mirror image of a class and then look for variable images for its fields and method images for its methods, getters, and setters (using declarations on the class mirror, instanceMembers, or staticMembers methods). With these mirrors, it is possible to further find the mirror of the class, such as the mirror of the parameter type of the given method mirror, and this process can be repeated many times. This means that naively providing support for mirroring all reachable classes could easily result in all classes in your program being included, although this may not be a good choice. Because of this, we chose a class image that defaults to omitting all declared type annotations. If these class images really should be included, they must be explicitly requested. This was done with TypeanationQuantify.
The semantics of attaching a reflector containing Typeanannotation Quantify(transient) to class C is that the included class mirror set is enhanced and all classes are used as type annotations in the included members. That is, the collection of included classes is iterated over, and each contained member of those classes is checked (for example, a method is included if it matches a given regular expression, or carries metadata of a given type, if the corresponding capability requires such an argument). For each parameter and return value of the method, any given type annotation is a class that is added to the contained set of classes. If transient is false or omitted, this process is run only once, and if transient is true, until no more classes are added.
Expansion of the set of overridden classes based on type annotations, either in a single step or fixed-point iteration, occurs in the third phase, after fixed-point iteration of subtypes and superclasses.
The semantics of attaching a reflector containing admitSubtype to a class C are subtle and worth discussing in a little more detail in the next section. The basic idea is that it allows instances of subtypes of the target class to be treated as instances of the target class.
Finally, we support “side tags” with global quantifiers, globalQuantify(RegExp, Reflector), and globalQuantifyMeta(MetadataClass, Reflector). At present, we must determine their as metadata attached to import package: reflectable/reflectable dart import statements, but if the other location in practice, we could relax the restrictions. Because of the monotonous semantics of capabilities, if a given program containing multiple such globalMetadata is not a problem, the reflection support provided will only satisfy the minimum of all requests.
The semantics of having globalQuantify(RegExp, Reflector) in a program are the same as the semantics of attaching a given reflector directly to a program for each class that qualifies its name to match a given RegExp. Similarly, the semantics of having globalQuantifyMeta(MetadataClass, reflector) in a program are the same as those of attaching a given reflector directly to metadata for each class that includes instances or subtypes of the MetadataClass type.
Contains members and no such methods
In general, coverage is based on bottom-up semantics. Within a given set of capabilities, the set of covered classes and the set of covered members within them is computed as a query against a given program. This is a bottom-up semantics because it starts with an empty override and then extends the override with concrete elements that do exist in the program.
Consider a member of the overwritten class. If it does not meet the override criteria (its name does not conform to a given regular expression, nor does it have any required metadata type), it is in the same state as a class or member name that does not have any declaration at all. When a method is called with a given actual argument list L, if its formal argument list does not allow it to be called with L as an actual argument, the method is considered nonexistent even if there is a declaration of a method with the specified name. For example, even if void foo(int I) exists, if we encounter a call to foo(0, bar: true), it is a nonexistent method event.
The key difference between this semantics and the semantics associated with the no-such-method event of a local Dart call is that the method in question may be completely missing or may be denied coverage because a given capability is too strict. Therefore, in order to enable programmers to properly handle cases where there is no such method called by reflection, we treat these cases differently than if there is no such method locally. Native call failure can lead to noSuchMethod is called to the same receiver, the Invocation parameters described the selector and parameters, and reflection calls failures can cause ReflectableNoSuchMethodError thrown. This type of exception contains a StringInvocation that specifies the same Invocation information as the Invocation, except that the member name is a string instead of a symbol. Programmers can catch this call and react in any appropriate way, for example, by calling their own variant of the noSuchMethod.
Instances of full or partial mirroring?
Traditionally, reflexive access to an instance, a class, or some other entity is thought to provide a complete and faithful view of that entity. For example, reflective code should be able to access features that are declared private, even if the reflective code is in an environment where non-reflective access to the same features is not allowed. Furthermore, when reflective look-ups are used to learn which class instances a given object is, we want the response to describe the actual runtime type of the object, not some superclass, such as the statically known type of the object in some context.
In the package reflectable, there are reasons for violating this integrity assumption, some of which are inherent consequences of having the package in the first place. In other words, these restrictions won’t disappear entirely. Other restrictions may be removed in the future because they were introduced based on certain trade-offs made during the implementation of the package.
The main motivation for providing the Reflectable package is that the more general support for reflection provided by the Dart :mirrors package tends to be too costly at run time in terms of program size, or perhaps have the resource impact of Dart :mirrors, so dart:mirrors support is omitted entirely. Thus, a core point of the package Reflectable is to specify a restricted version of reflection that fits the purpose of a particular program so that it can be done in a significantly smaller space. Therefore, it is perfectly normal for such a program to have reflection support for an object without reflection access to some of its methods. There are several other incomplete overwrites by design that aren’t a problem: they were part of the reason the package Reflectable was used in the first place.
The following sections discuss two different cases in which some limitations are not designed to exist. We first discuss incomplete access to proprietary features, and then we discuss the consequences of admitting subtypes specified with admitSubtype(apiSelection*).
Privacy – related restrictions
The limitations discussed in this section are caused by trade-offs in the implementation of package Reflectable, so we need to mention some implementation details. The package Reflectable is designed for code generation. The code generator takes a program (using the package reflectable) as input, generates code that supports the required reflection functionality, creates a “database” of expressions using a mirror, and consults that database at run time, all using plain, non-reflective code.
Ordinary code cannot violate privacy restrictions. Therefore, reflection operations provided by the package reflectable cannot, for example, read or write a private field that is different from the library containing the associated generated code. But current code generation methods always and only generate a new library that contains all generated code; This means that no private declarations in the program can be reached from the generated code, even the private declarations in the current package.
In principle, you can modify all libraries in the current package, but even if this could be used to access local private declarations, it would still make all private declarations in the imported package unreachable. However, this can only help if solutions are not so urgently needed. Libraries in local packages can often be edited to add an appropriate public declaration to give members access that would otherwise be inaccessible.
There are a few exceptions. The mirror image of the private class can be obtained from the image of the enclosing library, and the private class in the superclass chain is retained so that iteration over all the superclasses can be done if superclass quantization is required. But these private classes do not support static method calls, nor do they support mirroring on instances.
Considerations surrounding acceptance of subtypes
When targetMetadata in the form of apiSelection* is attached to a given class C, the effect is to provide reflection support for instances of class C and C. However, this support can be extended to provide partial reflection support for instances of subtypes of C in a way that incurs no further cost in terms of program size. A mirror generated for an instance of class C can have a reflection object (the object reflected by the image) of an appropriate subtype of C. It enables the instance image to hold a reflector that is an instance of the appropriate subtype of the type generated by the image.
The question is which instance image should be used for a given object O of runtime type D when there is no mirror class created just for D, which object is reflected as a parameter to the operation on the reflector. In general, there may be multiple candidate mirror classes corresponding to C1, C2,…… Ck classes, which are “minimum supertypes of D”, i.e., no type E is a proper supertype of D and a proper subtype of Ci for any I (which also means that no two Ci and Cj classes are subtypes of each other). The language specification includes an algorithm that will find a uniquely determined C1… Ck supertypes, which are called their minimum upper bounds. We can’t use this algorithm directly because we have an arbitrary subset of types in a type hierarchy, not all types, and then we need to make a similar decision about this “sparse” subtype hierarchy, which includes only reflective classes from a given reflector. Nevertheless, we hope that it will be possible to create a variant of the least upper bound algorithm that will apply to these sparse subtype hierarchies.
It should be noted that a very basic invariant that is generally assumed to support reflection in various languages has been violated. Of course, not all mirroring systems have a concept similar to mirroring built for a particular type, but the corresponding issues are relevant everywhere. The image does not report the as-is properties of the object; it reports the properties of instances of the supertype. This is an incompleteness that even causes the mirror to give an obviously incorrect description of the object in some cases.
In particular, given an object O of runtime type D, we have an instance mirror IM whose reflection object is O.
Let’s use reflection on IM to get a class mirror of O. Im. type will return an instance of C’s class image, CM, rather than the actual run-time type D of O. If a programmer uses this method to query the class name of an object like O, the answer will be a simple error, it says “C” when it should say “D”. Similarly, if we were to traverse the superclass, we would never see class D, nor the intermediate class between D and C. A real example is serialization: if we look for declarations of fields in order to serialize reflectors, we will silently not include fields declared in ignored subclasses until D. Generally speaking, there are many unpleasant surprises waiting for naive users of this feature, so it should be considered an expert option.
Why not just do the “right thing” and return a class mirror for D? It is not possible to simply check the runtime type of reflectee in the implementation of the method type and provide a class mirror of D, because normally there is no class mirror of D. In fact, the whole point of having admitSubtype quantifiers is that it saves space, because potentially large quantum types of a given type can be partially reflected without generating a corresponding number of mirror classes.
To further clarify what it means to get “partial” reflection support, consider a few cases.
Reflexively calling instance methods declared in C or inherited from O in C will work as expected, and standard object-oriented method calls will ensure that the correct method implementation of O is called, which may be an implementation available in C or one of the appropriate subtypes of C.
Calls to instance methods of O declared in an appropriate subtype of C, including methods from D itself, will not work. This is because IM’s class is generated under the assumption that no such method exists, and it only knows about C’s methods. As mentioned earlier, if we get the class of O, we might get the appropriate supertype of the actual class of O, so all derived operations would be similarly affected. For example, the declarations from CM will be declarations in C, and they have nothing to do with declarations in D. Similarly, if we were to traverse the superclass, we would only see the strict suffixes for the actual superclass list of class O.
Based on these serious issues, we decided that when instance mirroring is admitSubtype quantifier associated, it would be a runtime error to perform type methods in order to get a mirror of a class because when that class is not actually the class being mirrored, it will likely not work as expected. Again, in this case, the claim is not supported. It is possible to allow it if the match is just perfect (C == D), but it is difficult for programmers to use if they want to reflect a class that doesn’t come directly from an instance, they might as well use reflectType(C) directly.
In summary, there is a subtle trade-off to be made in cases where the entire subtype hierarchy should be equipped with reflection support. The tradeoff is either to pay the price in program size and get full support (using subtypeQuantify); Either aggressively save space in return for tolerating partial support for reflection (using admitSubtype).
conclusion
We have described the design of the capability used in package Reflectable to specify the expected level of support for reflection. The basic idea is that the capabilities of the base layer specify the operations to be selected from the API of the mirror class, as well as some simple restrictions on the allowed parameters for those operations. On this basis, apI-based capabilities can be associated with specific parts of the target program (although only classes at this point), so that it is those classes that will have the reflection support specified by apI-based capabilities. You can select the target class individually by adding a reflector as metadata on each target class. Alternatively, you can select the target class by quantization. For example, all subtypes can be quantized, in which case not only the class C that holds the metadata will be supported by reflection, but all subtypes of C will be supported by reflection. Finally, it is possible to accept instances of subtypes as reflectors for a small number of mirrors, so that partial reflection support can be implemented for many classes without having to spend many mirror classes.
reference
- Gilad Bracha and David Ungar. “Mirroring: Design principles for meta-facilities in object-oriented programming languages.” ACM SIGPLAN announcement. October 24, 2004. 331-344.
- Smith, Brian Cantwell. “Programmatic reflection in programming languages.” 1982.
- Sobel, Jonathan M. and Friedman, Daniel P. “Introduction to Reflective Programming.” Essays on Reflection. April 1996.
www.deepl.com translation