Тип Proxy
Последним элементом, необходимым для реализации abi.encode, является тип Proxy:
data Proxy(T) = Proxy;
Как и в определении типа memory, параметр типа здесь является фантомным, однако, в отличие от memory, Proxy не несёт никакой дополнительной информации во время выполнения. Он существует исключительно как маркерный тип, позволяющий передавать информацию на этапе компиляции. Подобные типы полностью «бесплатны» с точки зрения производительности: они полностью элиминируются на этапе компиляции и вовсе не присутствуют в финальной скомпилированной программе.
Хотя Proxy может показаться довольно экзотическим инструментом, он чрезвычайно полезен и даёт нам значительный контроль над выводом типов и выбором экземпляров классов типов без необходимости передавать данные во время выполнения там, где они не нужны. Такой подход часто используется как в Haskell (где он тоже называется Proxy), так и в Rust (std::marker::PhantomData).
abi.encode
Теперь мы готовы реализовать функцию abi.encode из классического Solidity на языке SAIL. Начнём с определения класса типов для метаданных, связанных с ABI. Заметим, что поскольку этому классу не нужно знать фактическое значение передаваемого типа, мы используем Proxy, чтобы сделать реализацию максимально лёгкой.
forall T . class T:ABIAttribs {
// how many bytes should be used for the head portion of the ABI encoding of `T`
function headSize(ty : Proxy(T)) -> word;
// whether or not `T` is a fully static type
function isStatic(ty : Proxy(T)) -> bool;
}
instance uint256:ABIAttribs {
function headSize(ty : Proxy(uint256)) -> word { return 32; }
function isStatic(ty : Proxy(uint256)) -> bool { return true; }
}Теперь определим ещё один класс, отвечающий за низкоуровневую запись в память. Приведённый здесь класс содержит некоторые дополнительные детали, необходимые для кодирования составных и динамических типов, которые не требуются для простого случая uint256, рассматриваемого сейчас. Мы показываем полную сложность, чтобы продемонстрировать, что наша система способна справляться и с более сложными случаями.
// types that can be abi encoded
forall T . T:ABIAttribs => class T:ABIEncode {
// abi encodes an instance of T into a memory region starting at basePtr
// offset gives the offset in memory from basePtr to the first empty byte of the head
// tail gives the position in memory of the first empty byte of the tail
function encodeInto(x : T, basePtr : word, offset : word, tail : word) -> word /* newTail */;
}
instance uint256:ABIEncode {
// a unit256 is written directly into the head
function encodeInto(x : uint256, basePtr : word, offset : word, tail : word) -> word {
let repx : word = Typedef.rep(x);
assembly { mstore(add(basePtr, offset), repx) }
return tail;
}
}
Наконец, мы можем определить высокоуровневую функцию abi_encode, которая занимается первоначальным выделением памяти и обновлением указателя на свободную память (реализация вспомогательных низкоуровневых функций get_free_memory и set_free_memory опущена для краткости):
// top level encoding function.
// abi encodes an instance of `T` and returns a pointer to the result
forall T . T:ABIEncode => function abi_encode(val : T) -> memory(bytes) {
let free = get_free_memory();
let headSize = ABIAttribs.headSize(Proxy : Proxy(T));
let tail = ABIEncode.encodeInto(val, free, 0, Add.add(free, headSize));
set_free_memory(tail);
return memory(free);
}
#core