Сравнение Solidity и FunC
Разработка смарт-контрактов предполагает знание таких языков, как Solidity для Ethereum и FunC для TON. Solidity - это объектно-ориентированный, высокоуровневый, строго типизированный язык, основанный на C++, Python и JavaScript, и специально разработанный для написания смарт-контрактов, которые выполняются на блокчейн-платформе Ethereum.
FunC также является языком высокого уровня, используемым для программирования смарт-контрактов на блокчейне TON, объектно-ориентированный, C-подобный, статически типизированный язык.
В следующих разделах будет приведен краткий анализ и сравнение двух языков по типам данных, хранилищам, функциям, структурам управления потоком и словарям, хеш-картам.
Схема хранения
Solidity обеспечивает плоскую модель хранения, которая означает, что все переменные состояния хранятся в едином непрерывном блоке памяти, называемом хранилищем. Хранилище состоит из набора данных типа ключ-значение, где каждый ключ есть 256-битное (32-байтовое) целое число, представляющее собой номер ячейки хранения, а каждое значение – это 256-битное слово, хранящееся в этой ячейке. Ячейки нумеруются последовательно, начиная с нуля, и в каждой ячейке может храниться только одно слово. Solidity позволяет разработчику сформировать структуру хранилища, используя ключевое слово storage для определения переменных состояния. Порядок, в котором задаются переменные, определяет их положение в хранилище.
Данные постоянного хранилища в блокчейне TON хранятся в виде ячеек. Ячейки играют роль памяти в стековой TVM. Ячейка может быть преобразована в фрагмент, Slice, а затем биты данных и ссылки на другие ячейки из ячейки могут быть получены путем их загрузки из этого фрагмента. Биты данных и ссылки на другие ячейки могут быть сохранены в компоновщике, Builder'e, а затем компоновщик может быть преобразован в новую ячейку.
Типы данных
Solidity включает следующие базовые типы данных:
- Целые числа со знаком/без знака
- Булевые значения
- Адреса — используются для хранения адресов кошельков Ethereum или смарт-контрактов, обычно около 20 байт. Тип адреса может быть дополнен ключевым словом
payable
, что ограничивает его использование хранением только адресов кошельков, а также только использованием функций передачи и отправки криптовалюты. - Массивы байтов — объявляются ключевым словом "bytes", представляют собой массив фиксированного размера, используем ый для хранения предопределенного количества байтов до 32, обычно задаются вместе с ключевым словом.
- Литералы – неизменяемые значения, такие как адреса, рациональные и целые числа, строки, юникод и шестнадцатеричные числа, которые могут храниться в переменной.
- Перечисления
- Массивы (фиксированные/динамические)
- Структуры
- Маппинги
В случае FunC основными типами данных являются:
- Целые числа
- Cell — базовая для TON непрозрачная структура данных, которая содержит до 1023 бит и до 4 ссылок на другие ячейки
- Slice и Builder — специальные объекты для чтения и записи в ячейки
- Continuation — еще одна разновидность ячейки, которая содержит готовый к выполнению байт-код TVM
- Tuples — упорядоченная коллекция из до 255 компонентов, имеющих произвольные типы значений, возможно, различные.
- Tensors — это упорядоченная коллекция, готовая к массовому присвоению типа: (int, int) a = (2, 4). Частным случаем тензорного типа является тип unit (). Он означает, что функция не возвращает никакого значения или не имеет аргументов.
В настоящее время FunC не поддерживает определение пользовательских тип ов.
См. также
Объявление и использование переменных
Solidity - это язык со статической типизацией, что означает, что тип каждой переменной должен быть указан при ее объявлении.
uint test = 1; // Declaring an unsigned variable of integer type
bool isActive = true; // Logical variable
string name = "Alice"; // String variable
FunC - более абстрактный и функционально-ориентированный язык, он поддерживает динамическую типизацию и функциональный стиль программирования.
(int x, int y) = (1, 2); // A tuple containing two integer variables
var z = x + y; // Dynamic variable declaration
См. также
Циклы
Solidity поддерживает циклы for
, while
и do { ... } while
.
Если вы хотите сделать что-то 10 раз, вы можете сделать это следующим образом:
uint x = 1;
for (uint i; i < 10; i++) {
x *= 2;
}
// x = 1024
FunC, в свою очередь, поддерживает циклы repeat
, while
и do { ... } until
. Цикл for не поддерживается. Если вы хотите выполнить тот же код, что и в примере выше, но на Func, вы можете использовать repeat
int x = 1;
repeat(10) {
x *= 2;
}
;; x = 1024
См. также
Функции
Подход Solidity к об ъявлениям функций является сочетанием ясности и контроля. В этом языке программирования каждая функция инициируется ключевым словом "function", за которым следует имя функции и ее параметры. Тело функции заключено в фигурные скобки, явно определяющие область действия. Кроме того, возвращаемые значения указываются с помощью ключевого слова "returns".Основное отличие Solidity заключается в определении области видимости функций. Они могут быть обозначены как public
, private
, internal
или external
, тем самым описывая условия, при которых к ним может быть получен доступ и осуществлен вызов из других частей смарт-контракта или внешних сущностей. Ниже приведен пример, в котором мы задаем глобальную переменную num
в языке Solidity:
function set(uint256 _num) public returns (bool) {
num = _num;
return true;
}
В свою очередь программа, написанная на FunC, по сути представляет собой список объявляемых функций и глобальных переменных. Объявление функции в FunC обычно начинается с необязательного декларатора, за которым следует возвращаемый тип и имя функции. Далее перечисляются параметры, а объявление заканчивается выбором спецификаторов, таких как impure
, inline/inline_ref
и method_id
.Эти спецификаторы регулируют область видимости функции, ее способность изменять хранилище контрактов и ее поведение при встраивании.Ниже приведен пример, в котором мы сохраняем переменную хранилища как ячейку в постоянном хранилище на языке Func:
() save_data(int num) impure inline {
set_data(begin_cell()
.store_uint(num, 32)
.end_cell()
);
}
См. также
Операторы выбора и перехода
Б ольшинство операторов из известных языков с фигурными скобками доступны и в Solidity, включая: if
, else
, while
, do
, for
, break
, continue
, return
, с обычной семантикой, известной из C или JavaScript.
FunC поддерживает классические операторы if-else
, а также циклы ifnot
, repeat
, while
и do/until
. Также с версии v0.4.0 поддерживаются операторы try-catch
.
См. также
Словари
Словари, hashmap/mapping, крайне важны для разработки контрактов Solidity и FunC, так как позволяют разработчикам эффективно хранить и извлекать данные в смарт-контрактах. В частности данные, связанные с определенным ключом, к примеру, баланс счета пользователя или факт владения активом.
Mapping — это хэш-таблица в Solidity, которая хранит данные в виде пар ключ-значение, где ключ может прин имать значение любого из встроенных типов данных, кроме ссылочного. В свою очередь значение может быть любым, даже ссылочным. Mapping чаще всего используются в Solidity и блокчейне Ethereum для соединения уникального адреса Ethereum с соответствующим типом значения. В любом другом языке программирования Mapping является эквивалентом словарю.
В Solidity структура Mapping не имеет размера и не имеет функционала задания ключа или значения. Mapping применим только к переменным состояния, которые служат типами ссылок на хранилище. Когда происходит инициализация Mapping структуры, он включает в себя все возможные ключи, которые соединены с значениями, байтовые представления которых состоят из одних нулей.
Аналогом Mapping в FunC являются словари или TON hashmap. В контексте TON, hashmap представляет собой структуру данных, представленную деревом ячеек. Hashmap маппит ключи в значения произвольного типа, чтобы обеспечить возможность их быстрого поиска и изменения. Абстрактное представление hashmap в TVM — это дерево Patricia или компактное двоичное дерево.Работа с потенциально большими деревьями ячеек может содержать несколько сложностей. Каждая операция обновления создает значительное количество ячеек, а каждая построенная ячейка стоит 500 единиц газа, что в свою очередь означает, что эти операции могут исчерпать имеющиеся ресурсы, если их использовать неосторожно.Чтобы избежать превышения лимита газа, ограничьте количество обновлений словаря за одну транзакцию. Кроме того, двоичное дерево для N
пар ключ-значение содержит N-1
форков, что означает в общей сложности не менее 2N-1
ячеек. Хранилище смарт-контракта ограничено 65536
уникальными ячейками, поэтому максимальное количество записей в словаре составляет 32768
или чуть больше, если есть повторяющиеся ячейки.
См. также
Взаимодействие смарт-контрактов
Solidity и FunC предоставляют разные подходы к взаимодействию со смарт-контрактами. Основное различие заключается в механизмах вызова и взаимодействия между контрактами.
Solidity использует объектно-ориентированный подход, при котором контракты взаимодействуют друг с другом посредством вызовов методов. Это похоже на вызовы методов в традиционных объектно-ориентированных языках программирования.
// External contract interface
interface IReceiver {
function receiveData(uint x) external;
}
contract Sender {
function sendData(address receiverAddress, uint x) public {
IReceiver receiver = IReceiver(receiverAddress);
receiver.receiveData(x); // Direct call of the contract function
}
}
FunC, используемый в экосистеме блокчейна TON, работает с сообщениями для вызова и взаимодействия между смарт-контрактами. Вместо прямого вызова методов контракты отправляют друг другу сообщения, которые могут содержать данные и код для выполнения.
Рассмотрим пример, в котором отправитель смарт-контракта должен отправить сообщение с номером, а получатель смарт-контракта должен получить этот номер и выполнить над ним некоторые манипуляции.
Изначально получатель смарт-контракта должен описать, как он будет получать сообщения.
() recv_internal(int my_balance, int msg_value, cell in_msg, slice in_msg_body) impure {
int op = in_msg_body~load_uint(32);
if (op == 1) {
int num = in_msg_body~load_uint(32);
;; do some manipulations
return ();
}
if (op == 2) {
;;...
}
}
Давайте подробнее обсудим, как выглядит получение сообщения в нашем целевом контракте:
recv_internal()
- эта функция выполняется, когда к контракту обращаются напрямую в блокчейне. Например, когда контракт обращается к нашему контракту.- Функция принимает сумму баланса контракта, сумму входящего сообщения, ячейку с исходным сообщением и срез
in_msg_body
, в котором хранится только тело полученного сообщения. - Наше тело сообщения будет хранить два целых числа. Первое число — это 32-битное беззнаковое целое число
op
, определяющее операцию, которую нужно выполнить, илиmethod
смарт-контракта, который нужно вызвать. Можно провести аналогию с Solidity и представить себеop
как сигнатуру функции. Второе число — это число, с которым нам нужно произвести некоторые манипуляции. - Чтобы прочитать из полученного Slice
op
инаше число
, мы используемload_uint()
. - Далее мы уже взаимодействуем с числом (мы опустили эту функциональность в этом примере).
Далее смарт-контракт отправителя должен корректно отправить сообщение. Для этого используется send_raw_message
, который ожидает сериализованное сообщение в качестве аргумента.
int num = 10;
cell msg_body_cell = begin_cell().store_uint(1,32).store_uint(num,32).end_cell();
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice("EQBIhPuWmjT7fP-VomuTWseE8JNWv2q7QYfsVQ1IZwnMk8wL"a) ;; in the example, we just hardcode the recipient's address
.store_coins(0)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(msg_body_cell)
.end_cell();
send_raw_message(msg, mode);
Да вайте более подробно рассмотрим то, как выглядит отправка сообщения нашим смарт-контрактом получателю:
- Сначала нам нужно построить наше сообщение. Полную структуру отправки можно найти здесь. Мы не будем детально описывать процесс создания сообщения здесь – вы можете прочитать об этом по ссылке.
- Тело сообщения представляет собой ячейку. В
msg_body_cell
мы делаем:begin_cell()
- создаетBuilder
для будущей ячейки, сначалаstore_uint
- сохраняет первыйuint
вBuilder
(1 - это нашop
), затемstore_uint
- сохраняет второйuint
вBuilder
(num - это наш номер, которым мы будем манипулировать в контракте-получателе),end_cell()
- создает ячейку. - Чтобы прикрепить тело, которое придет в
recv_internal
в сообщении, мы ссылаемся на собранную ячейку в самом сообщении с помощьюstore_ref
. - Отправка сообщения.
В этом примере показано, как смарт-контракты могут общаться друг с другом.