Перейти к основному содержимому

Сравнение 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) {
;;...
}
}

Давайте подробнее обсудим, как выглядит получение сообщения в нашем целевом контракте:

  1. recv_internal() - эта функция выполняется, когда к контракту обращаются напрямую в блокчейне. Например, когда контракт обращается к нашему контракту.
  2. Функция принимает сумму баланса контракта, сумму входящего сообщения, ячейку с исходным сообщением и срез in_msg_body, в котором хранится только тело полученного сообщения.
  3. Наше тело сообщения будет хранить два целых числа. Первое число — это 32-битное беззнаковое целое число op, определяющее операцию, которую нужно выполнить, или method смарт-контракта, который нужно вызвать. Можно провести аналогию с Solidity и представить себе op как сигнатуру функции. Второе число — это число, с которым нам нужно произвести некоторые манипуляции.
  4. Чтобы прочитать из полученного Slice op и наше число, мы используем load_uint().
  5. Далее мы уже взаимодействуем с числом (мы опустили эту функциональность в этом примере).

Далее смарт-контракт отправителя должен корректно отправить сообщение. Для этого используется 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);

Давайте более подробно рассмотрим то, как выглядит отправка сообщения нашим смарт-контрактом получателю:

  1. Сначала нам нужно построить наше сообщение. Полную структуру отправки можно найти здесь. Мы не будем детально описывать процесс создания сообщения здесь – вы можете прочитать об этом по ссылке.
  2. Тело сообщения представляет собой ячейку. В msg_body_cell мы делаем: begin_cell() - создает Builder для будущей ячейки, сначала store_uint - сохраняет первый uint в Builder (1 - это наш op), затем store_uint - сохраняет второй uint в Builder (num - это наш номер, которым мы будем манипулировать в контракте-получателе), end_cell() - создает ячейку.
  3. Чтобы прикрепить тело, которое придет в recv_internal в сообщении, мы ссылаемся на собранную ячейку в самом сообщении с помощью store_ref.
  4. Отправка сообщения.

В этом примере показано, как смарт-контракты могут общаться друг с другом.

См. также