Skip to main content

Architecture

note

You can skip this part of the wiki if you are not interested in knowing how everything works internally

I'm only going to cover Echo's architecture (at least for now). Echo consists of several parts, such as: processor, util, core and annotations.

Annotation Processor

processor is an annotaton processor which processes @Root and @Echo annotations, then, it writes two files into modules' jar: moduleid.meta.echo and moduleid.ref.echo (where moduleid is modules' id).

Meta File

.meta.echo is a Version object in binary format, which is then used to check compatibility.

Reference File

.ref.echo is a ConcurrentHashMap<String, List<String>> in binary format, where key is path to some "original" class, and value is list of paths to "echo" classes that modify the original class.

Util

util contains utility classes.

Config Util

ConfigUtil has code to read/write objects from/to objects. Used by processor and core to read and write .meta.echo and .ref.echo.

File Util

FileUtil has code to manipulate file paths and read bytes from file.

Version

Version class is used to determine version of echo toolchain which module was compiled with, it is being written in moduleid.meta.echo (where moduleid is modules' id) in compile-time and then compared in run-time with core's version.

info

Version of echo toolchain is the same for every echo sub-project. So for processor-1.0 there is core-1.0.

Annotations

annotations contains Echo's annotations, which are used by processor, core and modules.

Root

Root is an annotation that is used to define module root. Takes module id.

Echo

Echo annotation annotates an echo class, it takes target class' path as an argument.

Shadow

Shadow is an annotation that marks a method or method in echo class as already-defined. Allows to trick Java into using an actual method with the same name and descriptor.

Insert

Insert annotation inserts code to the start of method. Takes name of target's method and it's descriptor.

Add

Add annotation inserts code at the tail of method. Takes name of target's method and it's descriptor.

After

After NOT-IMPLEMENTED

Before

Before NOT-IMPLEMENTED

caution

Until Echo v1 releases, more and more annotations will be added.

Core

core's main class - Echo. First, when it's initialized, it gets current version, global config and custom ClassLoader. Then, launcher runs load(String) method, which takes module id. In return it reads the .meta.echo file with the given id and compares it with runtime version. After that, it reads the .ref.echo file of that module and merges it into the global config, which maps it's items to ClassNode's (see this for more info). After all loading is done, launcher runs finish method, which takes the global config and starts the process of transformation (see this for more info).

Transformation Manager

TransformationManager cycles through all entries in the global config and calls Transformers's handle method, to destribute needed event across all registered transformers. Transformer is an SPI, which can be used to register your own transformers (check Transformer API for more info).

After all transformers have finished their work, DefineTransformer will be called to get the bytecode of modified original class (represented by ClassNode) and define it using the custom ClassLoader. Defining the class before JVM has loaded it is a great way to modify classes, unfortunately, it can only work before JVM has read that class, because redefenition is not allowed (unless you are using Java Agents to modify classes, but it's much more time consuming).