Skip to content

SC: Implement call wrapper to simplify making sync calls #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: sync_call_entry_func
Choose a base branch
from

Conversation

linh2931
Copy link
Member

@linh2931 linh2931 commented May 12, 2025

Change Description

API Changes

  • API Changes

This PR implements call_wrapper for contract authors to simplify making sync calls. Instead of using sync call host functions directly, it is recommend contract authors use call_wrapper.

Below is a full working example of caller contract making a sync call in the callee contract.

Calle header file "callee.hpp"

#include <eosio/call.hpp>
#include <eosio/eosio.hpp>

class [[eosio::contract]] callee : public eosio::contract{
public:
   using contract::contract;

   [[eosio::call]]
   uint32_t sum(uint32_t a, uint32_t b, uint32_t c);

   using sum_func           = eosio::call_wrapper<"sum"_n, &sync_call_callee::sum>;
   using sum_func_read_only = eosio::call_wrapper<"sum"_n,
                                                  &sync_call_callee::sum,
                                                  access_mode::read_only>;
   using sum_func_no_op     = eosio::call_wrapper<"sum"_n,
                                                  &sync_call_callee::sum,
                                                  access_mode::read_write,
                                                  support_mode::no_op>;
};

Callee implementation file, "callee.cpp":

#include "callee.hpp"

uint32_t callee::sum(uint32_t a, uint32_t b, uint32_t c) {
   return a + b + c;
}

Caller file:

#include <eosio/call.hpp>
#include <eosio/eosio.hpp>

class [[eosio::contract]] caller : public eosio::contract{
public:
   using contract::contract;

   [[eosio::action]]
   void dosum() {
      // One way to call `sum()`
      auto result = callee::sum_func{ "receiver"_n }(10, 20, 30); // "receiver"_n is the receiver contract name
      
      // Another way to call `sum()`
      callee::sum_func sum{ "receiver"_n };
      uint32_t result = sum(10, 20, 30);

      //  Call the read only variant. The protocol enforces read only sync calls not to modify the states.
      //  A read only sync call will be aborted if the states are modified.
      uint32_t result = callee::sum_func_read_only{ "receiver"_n }(10, 20, 30);

      // Call the no-op support mode variant. A `no-op` support sync call is a no op if the receiver contract
      // does not have a sync call entry point, the data header is unsupported, or the entry point returns
      // an invalid error code. An empty std::optional result indicates the call was a no-op; a non-empty
      // std::optional indicate the call completed. The caller should check if the result has a value or not.
      std::optional<uint32_t> result = callee::sum_func_no_op{ "receiver"_n}(10, 20, 30); 
   }
}

In comparison, a caller using host functions directly to make the same call would look like

#include <eosio/call.hpp>
#include <eosio/eosio.hpp>

class [[eosio::contract]] caller : public eosio::contract {
public:
   using contract::contract;

   [[eosio::action]]
   void dosum() {
      //------- Make a call
      call_data_header header{ .version = 0, .func_name = "sum"_n.value };
      const std::vector<char> data{ eosio::pack(std::forward_as_tuple(header, 10, 20, 30)) };
      auto ret_val_size = eosio::call("receiver"_n, 0, data.data(), data.size());
      std::vector<char> return_value;
      return_value.resize(ret_val_size);
      auto actual_size = eosio::get_call_return_value(return_value.data(), return_value.size());
      eosio::check(actual_size == ret_val_size, "actual_size not equal to ret_val_size");
      auto result = eosio::unpack<uint32_t>(return_value);

      // ----- make the call as read only
      call_data_header header{ .version = 0, .func_name = "sum"_n.value };
      const std::vector<char> data{ eosio::pack(std::forward_as_tuple(header, 10, 20, 30)) };
      auto ret_val_size = eosio::call("receiver"_n, 0x01, data.data(), data.size()); // 0x01 is read_only flag
      ...

      // -----  handle the case if "receiver"_n does not support sync calls
      call_data_header header{ .version = 0, .func_name = "sum"_n.value };
      const std::vector<char> data{ eosio::pack(std::forward_as_tuple(header, 10, 20, 30)) };
      auto ret_val_size = eosio::call("receiver"_n, 0, data.data(), data.size());
      if (ret_val_size < 0) {
         // do something
      }
   }
}

Documentation Additions

  • Documentation Additions

@linh2931 linh2931 requested review from heifner and spoonincode May 12, 2025 17:58
@linh2931 linh2931 linked an issue May 12, 2025 that may be closed by this pull request
@linh2931 linh2931 marked this pull request as draft May 13, 2025 14:52
@linh2931 linh2931 marked this pull request as ready for review May 20, 2025 18:30
@heifner
Copy link
Member

heifner commented May 23, 2025

In the PR description.
See // KevinH: comments

#include <eosio/call.hpp>
#include <eosio/eosio.hpp>

class [[eosio::contract]] caller : public eosio::contract{
public:
   using contract::contract;

   [[eosio::action]]
   void dosum() {
      // one way to call sum
// KevinH: I would remove the word `assume`
      auto result = callee::sum_func{ "calleercvr"_n }(10, 20, 30); // assume receiver contract is "calleercvr"_n
      
      // another way to call sum
// KevinH: This should be `calleercvr`, right?
      callee::sum_func sum{ "callee"_n };
      uint32_t result = sum(10, 20, 30);

      //  call the read only variant
// KevinH: maybe mention what read only means
      uint32_t result = callee::sum_func_read_only{ "calleercvr"_n }(10, 20, 30);

      // call the no op support mode variant. please note the return type is std::optional
// KevinH: maybe mention what no op support mode means and why it returns an optional. 
// KevinH: Indicate what an empty optional means.
      std::optional<uint32_t> result = callee::sum_func_no_op{ "calleercvr"_n}(10, 20, 30); 
   }
}

In the host functions example, I think it would be better to use std::forward_as_tuple instead of std::make_tuple. It doesn't matter for the ints in the example, but better not to illustrate bad habits. Less needful would be to update the tests as well.

@linh2931
Copy link
Member Author

In the PR description. See // KevinH: comments

#include <eosio/call.hpp>
#include <eosio/eosio.hpp>

class [[eosio::contract]] caller : public eosio::contract{
public:
   using contract::contract;

   [[eosio::action]]
   void dosum() {
      // one way to call sum
// KevinH: I would remove the word `assume`
      auto result = callee::sum_func{ "calleercvr"_n }(10, 20, 30); // assume receiver contract is "calleercvr"_n
      
      // another way to call sum
// KevinH: This should be `calleercvr`, right?
      callee::sum_func sum{ "callee"_n };
      uint32_t result = sum(10, 20, 30);

      //  call the read only variant
// KevinH: maybe mention what read only means
      uint32_t result = callee::sum_func_read_only{ "calleercvr"_n }(10, 20, 30);

      // call the no op support mode variant. please note the return type is std::optional
// KevinH: maybe mention what no op support mode means and why it returns an optional. 
// KevinH: Indicate what an empty optional means.
      std::optional<uint32_t> result = callee::sum_func_no_op{ "calleercvr"_n}(10, 20, 30); 
   }
}

In the host functions example, I think it would be better to use std::forward_as_tuple instead of std::make_tuple. It doesn't matter for the ints in the example, but better not to illustrate bad habits. Less needful would be to update the tests as well.

Thanks for your detailed comments! Updated the PR description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SC: Do not generate sync_call entry point function if the user provides one SC: Make a convenient sync call wrapper
2 participants