API Reference

API Reference


Try it out

Try it out in Candid UI

The API Reference example code can be found in Github repo icpp-demos.

CandidTypes

Candid / C++ conversion tables

CandidType Candid C++ Example
CandidTypeBool bool bool example
CandidTypeEmpty empty - -
CandidTypeFloat32 float32 float example
CandidTypeFloat64 float64 double example
CandidTypeInt8 int8 int8_t example
CandidTypeInt16 int16 int16_t example
CandidTypeInt32 int32 int32_t example
CandidTypeInt64 int64 int64_t example
CandidTypeInt int __int128_t example
CandidTypeNat8 nat8 uint8_t example
CandidTypeNat16 nat16 uint16_t example
CandidTypeNat32 nat32 uint32_t example
CandidTypeNat64 nat64 uint64_t example
CandidTypeNat nat __uint128_t example
CandidTypeNull null - -
CandidTypeOpt opt std::optional example
CandidTypePrincipal principal std::string example
CandidTypeRecord record custom example
CandidTypeText text std::string example
CandidTypeVec vec std::vector example
CandidTypeVariant variant custom example

CandidTypeBool

A class to convert between Candid bool and C++ bool.

Declaration:

// candid.h
class CandidTypeBool {
public:
  CandidTypeBool(bool *v);
  CandidTypeBool(const bool v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_bool.cpp

$ icp canister call demo demo_candid_type_bool '(true)' --environment local
(true)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_bool received value '1'

$ icp canister call demo demo_candid_type_bools '(true, false)' --environment local
(true, false)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_bools received values '1' & '0'

*/
#include "demo_candid_type_bool.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_bool() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  bool in{false};
  ic_api.from_wire(CandidTypeBool{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeBool{in});
}

void demo_candid_type_bools() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  bool in1{false};
  bool in2{false};
  CandidArgs args_in;
  args_in.append(CandidTypeBool(&in1));
  args_in.append(CandidTypeBool(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeBool(in1));
  args_out.append(CandidTypeBool(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_bool.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_bool()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_bool");
void demo_candid_type_bools()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_bools");
// file: src/demo.did
service : {
    "demo_candid_type_bool" : (bool) -> (bool) query;
    "demo_candid_type_bools" : (bool, bool) -> (bool, bool) query; 
};

CandidTypeFloat32

A class to convert between Candid float32 and C++ float.

Declaration:

// candid.h
class CandidTypeFloat32 {
public:
  CandidTypeFloat32(float *v);
  CandidTypeFloat32(const float v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_float32.cpp

$ icp canister call demo demo_candid_type_float32 '(0.1 : float32)' --environment local
(0.1 : float32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_float32 received value '0.100000'

$ icp canister call demo demo_candid_type_float32s '(0.1 : float32, -1.2 : float32)' --environment local
(0.1 : float32, -1.2 : float32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_float32s received values '0.100000' & '-1.200000'

*/
#include "demo_candid_type_float32.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_float32() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  float in{0.0};
  ic_api.from_wire(CandidTypeFloat32{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeFloat32{in});
}

void demo_candid_type_float32s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  float in1{0.0};
  float in2{0.0};
  CandidArgs args_in;
  args_in.append(CandidTypeFloat32(&in1));
  args_in.append(CandidTypeFloat32(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeFloat32(in1));
  args_out.append(CandidTypeFloat32(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_float32.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_float32()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_float32");
void demo_candid_type_float32s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_float32s");
// file: src/demo.did
service : {
    "demo_candid_type_float32" : (float32) -> (float32) query;
    "demo_candid_type_float32s" : (float32, float32) -> (float32, float32) query; 
};

CandidTypeFloat64

A class to convert between Candid float64 and C++ double.

Declaration:

// candid.h
class CandidTypeFloat64 {
public:
  CandidTypeFloat64(double *v);
  CandidTypeFloat64(const double v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_float64.cpp

$ icp canister call demo demo_candid_type_float64 '(0.1 : float64)' --environment local
(0.1 : float64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_float64 received value '0.100000'

$ icp canister call demo demo_candid_type_float64s '(0.1 : float64, -1.2 : float64)' --environment local
(0.1 : float64, -1.2 : float64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_float64s received values '0.100000' & '-1.200000'

*/
#include "demo_candid_type_float64.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_float64() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  double in{0.0};
  ic_api.from_wire(CandidTypeFloat64{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeFloat64{in});
}

void demo_candid_type_float64s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  double in1{0.0};
  double in2{0.0};
  CandidArgs args_in;
  args_in.append(CandidTypeFloat64(&in1));
  args_in.append(CandidTypeFloat64(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeFloat64(in1));
  args_out.append(CandidTypeFloat64(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_float64.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_float64()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_float64");
void demo_candid_type_float64s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_float64s");
// file: src/demo.did
service : {
    "demo_candid_type_float64" : (float64) -> (float64) query;
    "demo_candid_type_float64s" : (float64, float64) -> (float64, float64) query; 
};

CandidTypeInt8

A class to convert between Candid int8 and C++ int8_t.

Declaration:

// candid.h
class CandidTypeInt8 {
public:
  CandidTypeInt8(int8_t *v);
  CandidTypeInt8(const int8_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_int8.cpp

$ icp canister call demo demo_candid_type_int8 '(101 : int8)' --environment local
(101 : int8)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int8 received value '101'

$ icp canister call demo demo_candid_type_int8s '(101 : int8, -102 : int8)' --environment local
(101 : int8, -102 : int8)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int8s received values '101' & '-102'

*/
#include "demo_candid_type_int8.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_int8() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int8_t in{0};
  ic_api.from_wire(CandidTypeInt8{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeInt8{in});
}

void demo_candid_type_int8s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int8_t in1{0};
  int8_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeInt8(&in1));
  args_in.append(CandidTypeInt8(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeInt8(in1));
  args_out.append(CandidTypeInt8(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_int8.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_int8()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int8");
void demo_candid_type_int8s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int8s");
// file: src/demo.did
service : {
    "demo_candid_type_int8" : (int8) -> (int8) query;
    "demo_candid_type_int8s" : (int8, int8) -> (int8, int8) query; 
};

CandidTypeInt16

A class to convert between Candid int16 and C++ int16_t.

Declaration:

// candid.h
class CandidTypeInt16 {
public:
  CandidTypeInt16(int16_t *v);
  CandidTypeInt16(const int16_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_int16.cpp

$ icp canister call demo demo_candid_type_int16 '(101 : int16)' --environment local
(101 : int16)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int16 received value '101'

$ icp canister call demo demo_candid_type_int16s '(101 : int16, -102 : int16)' --environment local
(101 : int16, -102 : int16)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int16s received values '101' & '-102'

*/
#include "demo_candid_type_int16.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_int16() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int16_t in{0};
  ic_api.from_wire(CandidTypeInt16{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeInt16{in});
}

void demo_candid_type_int16s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int16_t in1{0};
  int16_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeInt16(&in1));
  args_in.append(CandidTypeInt16(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeInt16(in1));
  args_out.append(CandidTypeInt16(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_int16.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_int16()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int16");
void demo_candid_type_int16s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int16s");
// file: src/demo.did
service : {
    "demo_candid_type_int16" : (int16) -> (int16) query;
    "demo_candid_type_int16s" : (int16, int16) -> (int16, int16) query; 
};

CandidTypeInt32

A class to convert between Candid int32 and C++ int32_t.

Declaration:

// candid.h
class CandidTypeInt32 {
public:
  CandidTypeInt32(int32_t *v);
  CandidTypeInt32(const int32_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_int32.cpp

$ icp canister call demo demo_candid_type_int32 '(101 : int32)' --environment local
(101 : int32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int32 received value '101'

$ icp canister call demo demo_candid_type_int32s '(101 : int32, -102 : int32)' --environment local
(101 : int32, -102 : int32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int32s received values '101' & '-102'

*/
#include "demo_candid_type_int32.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_int32() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int32_t in{0};
  ic_api.from_wire(CandidTypeInt32{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeInt32{in});
}

void demo_candid_type_int32s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int32_t in1{0};
  int32_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeInt32(&in1));
  args_in.append(CandidTypeInt32(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeInt32(in1));
  args_out.append(CandidTypeInt32(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_int32.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_int32()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int32");
void demo_candid_type_int32s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int32s");
// file: src/demo.did
service : {
    "demo_candid_type_int32" : (int32) -> (int32) query;
    "demo_candid_type_int32s" : (int32, int32) -> (int32, int32) query; 
};

CandidTypeInt64

A class to convert between Candid int64 and C++ int64_t.

Declaration:

// candid.h
class CandidTypeInt64 {
public:
  CandidTypeInt64(int64_t *v);
  CandidTypeInt64(const int64_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_int64.cpp

$ icp canister call demo demo_candid_type_int64 '(101 : int64)' --environment local
(101 : int64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int64 received value '101'

$ icp canister call demo demo_candid_type_int64s '(101 : int64, -102 : int64)' --environment local
(101 : int64, -102 : int64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int64s received values '101' & '-102'

*/
#include "demo_candid_type_int64.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_int64() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int64_t in{0};
  ic_api.from_wire(CandidTypeInt64{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeInt64{in});
}

void demo_candid_type_int64s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  int64_t in1{0};
  int64_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeInt64(&in1));
  args_in.append(CandidTypeInt64(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeInt64(in1));
  args_out.append(CandidTypeInt64(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_int64.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_int64()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int64");
void demo_candid_type_int64s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int64s");
// file: src/demo.did
service : {
    "demo_candid_type_int64" : (int64) -> (int64) query;
    "demo_candid_type_int64s" : (int64, int64) -> (int64, int64) query; 
};

CandidTypeInt

A class to convert between Candid int and C++ __int128_t.

Declaration:

// candid.h
class CandidTypeInt {
public:
  CandidTypeInt(__int128_t *v);
  CandidTypeInt(const __int128_t &v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_int.cpp

$ icp canister call demo demo_candid_type_int '(101 : int)' --environment local
(101 : int)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_int received value '101'

$ icp canister call demo demo_candid_type_ints '(101 : int, -102 : int)' --environment local
(101 : int, -102 : int)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_ints received values '101' & '-102'

*/
#include "demo_candid_type_int.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_int() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  __int128_t in{0};
  ic_api.from_wire(CandidTypeInt{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   IC_API::to_string_128(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeInt{in});
}

void demo_candid_type_ints() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  __int128_t in1{0};
  __int128_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeInt(&in1));
  args_in.append(CandidTypeInt(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   IC_API::to_string_128(in1) + "' & '" +
                   IC_API::to_string_128(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeInt(in1));
  args_out.append(CandidTypeInt(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_int.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_int()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_int");
void demo_candid_type_ints()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_ints");
// file: src/demo.did
service : {
    "demo_candid_type_int" : (int) -> (int) query;
    "demo_candid_type_ints" : (int, int) -> (int, int) query; 
};

CandidTypeNat8

A class to convert between Candid int8 and C++ uint8_t.

Declaration:

// candid.h
class CandidTypeNat8 {
public:
  CandidTypeNat8(uint8_t *v);
  CandidTypeNat8(const uint8_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_nat8.cpp

$ icp canister call demo demo_candid_type_nat8 '(101 : nat8)' --environment local
(101 : nat8)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat8 received value '101'

$ icp canister call demo demo_candid_type_nat8s '(101 : nat8, 102 : nat8)' --environment local
(101 : nat8, 102 : nat8)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat8s received values '101' & '102'

*/
#include "demo_candid_type_nat8.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_nat8() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint8_t in{0};
  ic_api.from_wire(CandidTypeNat8{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeNat8{in});
}

void demo_candid_type_nat8s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint8_t in1{0};
  uint8_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeNat8(&in1));
  args_in.append(CandidTypeNat8(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeNat8(in1));
  args_out.append(CandidTypeNat8(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_nat8.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_nat8()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat8");
void demo_candid_type_nat8s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat8s");
// file: src/demo.did
service : {
    "demo_candid_type_nat8" : (nat8) -> (nat8) query;
    "demo_candid_type_nat8s" : (nat8, nat8) -> (nat8, nat8) query; 
};

CandidTypeNat16

A class to convert between Candid int16 and C++ uint16_t.

Declaration:

// candid.h
class CandidTypeNat16 {
public:
  CandidTypeNat16(uint16_t *v);
  CandidTypeNat16(const uint16_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_nat16.cpp

$ icp canister call demo demo_candid_type_nat16 '(101 : nat16)' --environment local
(101 : nat16)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat16 received value '101'

$ icp canister call demo demo_candid_type_nat16s '(101 : nat16, 102 : nat16)' --environment local
(101 : nat16, 102 : nat16)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat16s received values '101' & '102'

*/
#include "demo_candid_type_nat16.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_nat16() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint16_t in{0};
  ic_api.from_wire(CandidTypeNat16{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeNat16{in});
}

void demo_candid_type_nat16s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint16_t in1{0};
  uint16_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeNat16(&in1));
  args_in.append(CandidTypeNat16(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeNat16(in1));
  args_out.append(CandidTypeNat16(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_nat16.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_nat16()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat16");
void demo_candid_type_nat16s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat16s");
// file: src/demo.did
service : {
    "demo_candid_type_nat16" : (nat16) -> (nat16) query;
    "demo_candid_type_nat16s" : (nat16, nat16) -> (nat16, nat16) query; 
};

CandidTypeNat32

A class to convert between Candid int32 and C++ uint32_t.

Declaration:

// candid.h
class CandidTypeNat32 {
public:
  CandidTypeNat32(uint32_t *v);
  CandidTypeNat32(const uint32_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_nat32.cpp

$ icp canister call demo demo_candid_type_nat32 '(101 : nat32)' --environment local
(101 : nat32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat32 received value '101'

$ icp canister call demo demo_candid_type_nat32s '(101 : nat32, 102 : nat32)' --environment local
(101 : nat32, 102 : nat32)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat32s received values '101' & '102'

*/
#include "demo_candid_type_nat32.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_nat32() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint32_t in{0};
  ic_api.from_wire(CandidTypeNat32{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeNat32{in});
}

void demo_candid_type_nat32s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint32_t in1{0};
  uint32_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeNat32(&in1));
  args_in.append(CandidTypeNat32(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeNat32(in1));
  args_out.append(CandidTypeNat32(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_nat32.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_nat32()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat32");
void demo_candid_type_nat32s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat32s");
// file: src/demo.did
service : {
    "demo_candid_type_nat32" : (nat32) -> (nat32) query;
    "demo_candid_type_nat32s" : (nat32, nat32) -> (nat32, nat32) query; 
};

CandidTypeNat64

A class to convert between Candid int64 and C++ int64_t.

Declaration:

// candid.h
class CandidTypeNat64 {
public:
  CandidTypeNat64(uint64_t *v);
  CandidTypeNat64(const uint64_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_nat64.cpp

$ icp canister call demo demo_candid_type_nat64 '(101 : nat64)' --environment local
(101 : nat64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat64 received value '101'

$ icp canister call demo demo_candid_type_nat64s '(101 : nat64, 102 : nat64)' --environment local
(101 : nat64, 102 : nat64)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat64s received values '101' & '102'

*/
#include "demo_candid_type_nat64.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_nat64() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint64_t in{0};
  ic_api.from_wire(CandidTypeNat64{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   std::to_string(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeNat64{in});
}

void demo_candid_type_nat64s() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint64_t in1{0};
  uint64_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeNat64(&in1));
  args_in.append(CandidTypeNat64(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   std::to_string(in1) + "' & '" + std::to_string(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeNat64(in1));
  args_out.append(CandidTypeNat64(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_nat64.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_nat64()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat64");
void demo_candid_type_nat64s()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat64s");
// file: src/demo.did
service : {
    "demo_candid_type_nat64" : (nat64) -> (nat64) query;
    "demo_candid_type_nat64s" : (nat64, nat64) -> (nat64, nat64) query; 
};

CandidTypeNat

A class to convert between Candid int and C++ __uint128_t.

Declaration:

// candid.h
class CandidTypeNat {
public:
  CandidTypeNat(__uint128_t *v);
  CandidTypeNat(const __uint128_t v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_nat.cpp

$ icp canister call demo demo_candid_type_nat '(101 : nat)' --environment local
(101 : nat)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nat received value '101'

$ icp canister call demo demo_candid_type_nats '(101 : nat, 102 : nat)' --environment local
(101 : nat, 102 : nat)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_nats received values '101' & '102'

*/
#include "demo_candid_type_nat.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_nat() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  __uint128_t in{0};
  ic_api.from_wire(CandidTypeNat{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" +
                   IC_API::to_string_128(in) + "'"
            << std::endl;
  ic_api.to_wire(CandidTypeNat{in});
}

void demo_candid_type_nats() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  __uint128_t in1{0};
  __uint128_t in2{0};
  CandidArgs args_in;
  args_in.append(CandidTypeNat(&in1));
  args_in.append(CandidTypeNat(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" +
                   IC_API::to_string_128(in1) + "' & '" +
                   IC_API::to_string_128(in2) + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeNat(in1));
  args_out.append(CandidTypeNat(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_nat.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_nat()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nat");
void demo_candid_type_nats()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_nats");
// file: src/demo.did
service : {
    "demo_candid_type_nat" : (nat) -> (nat) query;
    "demo_candid_type_nats" : (nat, nat) -> (nat, nat) query; 
};

CandidTypeOpt

A group of classes to convert between Candid opt candid-type and C++ std::optional<C++ type>.

CandidTypeOpt Candid C++ Example
CandidTypeOptBool opt bool std::optional<bool> example
not supported opt empty not supported -
CandidTypeOptFloat32 opt float32 std::optional<float> example
CandidTypeOptFloat64 opt float64 std::optional<double> example
CandidTypeOptInt8 opt int8 std::optional<int8_t> example
CandidTypeOptInt16 opt int16 std::optional<int16_t> example
CandidTypeOptInt32 opt int32 std::optional<int32_t> example
CandidTypeOptInt64 opt int64 std::optional<int64_t> example
CandidTypeOptInt opt int std::optional<__int128_t> example
CandidTypeOptNat8 opt nat8 std::optional<uint8_t> example
CandidTypeOptNat16 opt nat16 std::optional<uint16_t> example
CandidTypeOptNat32 opt nat32 std::optional<uint32_t> example
CandidTypeOptNat64 opt nat64 std::optional<uint64_t> example
CandidTypeOptNat opt nat std::optional<__uint128_t> example
not supported opt null not supported -
CandidTypeOptPrincipal opt principal std::optional<std::string> example
CandidTypeOptText opt text std::optional<std::string> example

Declaration:

// candid.h
class CandidTypeOptBool {
public:
  CandidTypeOptBool(std::optional<bool> *v);
  CandidTypeOptBool(const std::optional<bool> v); 
}

class CandidTypeOptFloat32 {
public:
  CandidTypeOptFloat32(std::optional<float> *v);
  CandidTypeOptFloat32(const std::optional<float> v); 
}

class CandidTypeOptFloat64 {
public:
  CandidTypeOptFloat64(std::optional<double> *v);
  CandidTypeOptFloat64(const std::optional<double> v); 
}

class CandidTypeOptInt8 {
public:
  CandidTypeOptInt8(std::optional<int8_t> *v);
  CandidTypeOptInt8(const std::optional<int8_t> v); 
}

class CandidTypeOptInt16 {
public:
  CandidTypeOptInt16(std::optional<int16_t> *v);
  CandidTypeOptInt16(const std::optional<int16_t> v); 
}

class CandidTypeOptInt32 {
public:
  CandidTypeOptInt32(std::optional<int32_t> *p_v);
  CandidTypeOptInt32(const std::optional<int32_t> v); 
}

class CandidTypeOptInt64 {
public:
  CandidTypeOptInt64(std::optional<int64_t> *v);
  CandidTypeOptInt64(const std::optional<int64_t> v); 
}

class CandidTypeOptInt {
public:
  CandidTypeOptInt(std::optional<__int128_t> *v);
  CandidTypeOptInt(const std::optional<__int128_t> v); 
}

class CandidTypeOptNat8 {
public:
  CandidTypeOptNat8(std::optional<uint8_t> *v);
  CandidTypeOptNat8(const std::optional<uint8_t> v); 
}

class CandidTypeOptNat16 {
public:
  CandidTypeOptNat16(std::optional<uint16_t> *v);
  CandidTypeOptNat16(const std::optional<uint16_t> v); 
}

class CandidTypeOptNat32 {
public:
  CandidTypeOptNat32(std::optional<uint32_t> *v);
  CandidTypeOptNat32(const std::optional<uint32_t> v); 
}

class CandidTypeOptNat64 {
public:
  CandidTypeOptNat64(std::optional<uint64_t> *v);
  CandidTypeOptNat64(const std::optional<uint64_t> v); 
}

class CandidTypeOptNat {
public:
  CandidTypeOptNat(std::optional<__uint128_t> *v);
  CandidTypeOptNat(const std::optional<__uint128_t> v); 
}

class CandidTypeOptPrincipal {
public:
  CandidTypeOptPrincipal(std::optional<std::string> *p_v);
  CandidTypeOptPrincipal(const std::optional<std::string> v); 
}

class CandidTypeOptText {
public:
  CandidTypeOptText(std::optional<std::string> *v);
  CandidTypeOptText(const std::optional<std::string> v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_opt.cpp

// ----------------------------------------------------------
// The (opt bool) has three possible values:
// - true
// - false
// - no value
// Let's try them all out:

$ icp canister call demo demo_candid_type_opt '(opt (true : bool))' --environment local
(opt true)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opt received bool value '1'

$ icp canister call demo demo_candid_type_opt '(opt (false : bool))' --environment local
(opt false)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opt received bool value '0'

$ icp canister call demo demo_candid_type_opt '(null)' --environment local
(null)
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opt did not receive a bool value

// ----------------------------------------------------------
// The other method has as argument a list of all opt types, eg.

$ icp canister call demo demo_candid_type_opts '(opt (true : bool), opt (0.1 : float32), opt (0.2 : float64), opt (-8 : int8), opt (-16 : int16), opt (-32 : int32), opt (-64 : int64), opt (-128 : int), opt (8 : nat8), opt (16 : nat16), opt (32 : nat32), opt (64 : nat64), opt (128 : nat), opt (principal "2ibo7-dia"), opt ("demo" : text))' --environment local
(opt true, opt (0.1 : float32), opt (0.2 : float64), opt (-8 : int8), opt (-16 : int16), opt (-32 : int32), opt (-64 : int64), opt (-128 : int), opt (8 : nat8), opt (16 : nat16), opt (32 : nat32), opt (64 : nat64), opt (128 : nat), opt principal "2ibo7-dia", opt "demo")
-> check the console of the local network. The canister will print:
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received bool value '1'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received float32 value '0.100000'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received float64 value '0.200000'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received int8 value '-8'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received int16 value '-16'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received int32 value '-32'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received int64 value '-64'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received int value '-128'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received nat8 value '8'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received nat16 value '16'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received nat32 value '32'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received nat64 value '64'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received nat value '128'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received principal value '2ibo7-dia'
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_opts received text value 'demo'

*/
#include "demo_candid_type_opt.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_opt() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::optional<bool> in;
  ic_api.from_wire(CandidTypeOptBool{&in});

  if (in.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received bool value '" +
                     std::to_string(in.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a bool value"
              << std::endl;
  }

  ic_api.to_wire(CandidTypeOptBool{in});
}

void demo_candid_type_opts() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ---------------------------------------------------------------------------
  // Get the data from the wire
  std::optional<bool> in_bool;
  std::optional<float> in_float32;
  std::optional<double> in_float64;
  std::optional<int8_t> in_int8;
  std::optional<int16_t> in_int16;
  std::optional<int32_t> in_int32;
  std::optional<int64_t> in_int64;
  std::optional<__int128_t> in_int;
  std::optional<uint8_t> in_nat8;
  std::optional<uint16_t> in_nat16;
  std::optional<uint32_t> in_nat32;
  std::optional<uint64_t> in_nat64;
  std::optional<__uint128_t> in_nat;
  std::optional<std::string> in_principal;
  std::optional<std::string> in_text;

  CandidArgs args_in;
  args_in.append(CandidTypeOptBool{&in_bool});
  args_in.append(CandidTypeOptFloat32{&in_float32});
  args_in.append(CandidTypeOptFloat64{&in_float64});
  args_in.append(CandidTypeOptInt8{&in_int8});
  args_in.append(CandidTypeOptInt16{&in_int16});
  args_in.append(CandidTypeOptInt32{&in_int32});
  args_in.append(CandidTypeOptInt64{&in_int64});
  args_in.append(CandidTypeOptInt{&in_int});
  args_in.append(CandidTypeOptNat8{&in_nat8});
  args_in.append(CandidTypeOptNat16{&in_nat16});
  args_in.append(CandidTypeOptNat32{&in_nat32});
  args_in.append(CandidTypeOptNat64{&in_nat64});
  args_in.append(CandidTypeOptNat{&in_nat});
  args_in.append(CandidTypeOptPrincipal{&in_principal});
  args_in.append(CandidTypeOptText{&in_text});

  ic_api.from_wire(args_in);

  // ---------------------------------------------------------------------------
  // print the data
  if (in_bool.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received bool value '" +
                     std::to_string(in_bool.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a bool value"
              << std::endl;
  }

  if (in_float32.has_value()) {
    std::cout << "Method " + std::string(__func__) +
                     " received float32 value '" +
                     std::to_string(in_float32.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a float32 value"
              << std::endl;
  }

  if (in_float64.has_value()) {
    std::cout << "Method " + std::string(__func__) +
                     " received float64 value '" +
                     std::to_string(in_float64.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a float64 value"
              << std::endl;
  }

  if (in_int8.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received int8 value '" +
                     std::to_string(in_int8.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a int8 value"
              << std::endl;
  }

  if (in_int16.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received int16 value '" +
                     std::to_string(in_int16.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a int16 value"
              << std::endl;
  }

  if (in_int32.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received int32 value '" +
                     std::to_string(in_int32.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a int32 value"
              << std::endl;
  }

  if (in_int64.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received int64 value '" +
                     std::to_string(in_int64.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a int64 value"
              << std::endl;
  }

  if (in_int.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received int value '" +
                     IC_API::to_string_128(in_int.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a int value"
              << std::endl;
  }

  if (in_nat8.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received nat8 value '" +
                     std::to_string(in_nat8.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a nat8 value"
              << std::endl;
  }

  if (in_nat16.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received nat16 value '" +
                     std::to_string(in_nat16.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a nat16 value"
              << std::endl;
  }

  if (in_nat32.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received nat32 value '" +
                     std::to_string(in_nat32.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a nat32 value"
              << std::endl;
  }

  if (in_nat64.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received nat64 value '" +
                     std::to_string(in_nat64.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a nat64 value"
              << std::endl;
  }

  if (in_nat.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received nat value '" +
                     IC_API::to_string_128(in_nat.value()) + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a nat value"
              << std::endl;
  }

  if (in_principal.has_value()) {
    std::cout << "Method " + std::string(__func__) +
                     " received principal value '" + in_principal.value() + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a principal value"
              << std::endl;
  }

  if (in_text.has_value()) {
    std::cout << "Method " + std::string(__func__) + " received text value '" +
                     in_text.value() + "'"
              << std::endl;
  } else {
    std::cout << "Method " + std::string(__func__) +
                     " did not receive a text value"
              << std::endl;
  }

  // ---------------------------------------------------------------------------
  // Return the data
  CandidArgs args_out;
  args_out.append(CandidTypeOptBool{in_bool});
  args_out.append(CandidTypeOptFloat32{in_float32});
  args_out.append(CandidTypeOptFloat64{in_float64});
  args_out.append(CandidTypeOptInt8{in_int8});
  args_out.append(CandidTypeOptInt16{in_int16});
  args_out.append(CandidTypeOptInt32{in_int32});
  args_out.append(CandidTypeOptInt64{in_int64});
  args_out.append(CandidTypeOptInt{in_int});
  args_out.append(CandidTypeOptNat8{in_nat8});
  args_out.append(CandidTypeOptNat16{in_nat16});
  args_out.append(CandidTypeOptNat32{in_nat32});
  args_out.append(CandidTypeOptNat64{in_nat64});
  args_out.append(CandidTypeOptNat{in_nat});
  args_out.append(CandidTypeOptPrincipal{in_principal});
  args_out.append(CandidTypeOptText{in_text});
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_opt.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_opt()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_opt");
void demo_candid_type_opts()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_opts");
// file: src/demo.did
service : {
    "demo_candid_type_opt" : (opt bool) -> (opt bool) query;
    "demo_candid_type_opts" : (opt bool, opt float32, opt float64, opt int8, opt int16, opt int32, opt int64, opt int, opt nat8, opt nat16, opt nat32, opt nat64, opt nat, opt principal, opt text) -> 
                              (opt bool, opt float32, opt float64, opt int8, opt int16, opt int32, opt int64, opt int, opt nat8, opt nat16, opt nat32, opt nat64, opt nat, opt principal, opt text)
                              query; 
};

CandidTypePrincipal

A class to convert between Candid principal and C++ std::string.

The value of the string follows the ID Encoding Specification.

Declaration:

// candid.h
class CandidTypePrincipal {
public:
  CandidTypePrincipal(const std::string v);

  // Checks if the user is anonymous
  // If the user is not anonymous, that means that the user is authenticated 
  // with Internet Identity or other mechanisms that assign a unique 
  // principal ID.
  bool is_anonymous(); 

  // Get the principal ID in text format
  std::string get_text(); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_principal.cpp

$ icp canister call demo demo_candid_type_principal '(principal "2ibo7-dia")' --environment local
(principal "2ibo7-dia")
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_principal received value '2ibo7-dia'

$ icp canister call demo demo_candid_type_principals '(principal "2ibo7-dia", principal "w3gef-eqbai")' --environment local
(principal "2ibo7-dia", principal "w3gef-eqbai")
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_principals received values '2ibo7-dia' & 'w3gef-eqbai'

*/
#include "demo_candid_type_principal.h"

#include <iostream>
#include <string>

#include "ic_api.h"

// Whitelisted principals, saved in Orthogonal Persisted Memory
std::vector<std::string> whitelisted_principals = {
    "expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae", // User
    "iineg-fibai-bqibi-ga4ea-searc-ijrif-iwc4m-bsibb-eirsi-jjge4-ucs", // User
    "6uwoh-vaaaa-aaaag-amema-cai", // Another canister
};

void demo_candid_type_principal() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  // This demo shows how to implement whitelisting of users.

  // 1. Get the caller's authenticated principal
  // - The calling message was signed using the caller's private key
  // - The IC verified the signature using the corresponding public key
  CandidTypePrincipal caller = ic_api.get_caller();

  // 2. Check if the user is anonymous
  // If the user is not anonymous, that means that the user is authenticated
  // with Internet Identity or other mechanisms that assign a unique
  // principal ID.
  if (caller.is_anonymous()) {
    std::cout << "User is not logged in " << std::endl;
  } else {
    std::cout << "User is logged in " << std::endl;
  }

  // 3. Get the caller's principal id in text format
  std::string principal_id_caller = caller.get_text();
  std::cout << "User's principal id is " << principal_id_caller << std::endl;

  // 4. Check if the caller's principal is whitelisted using std::find
  auto it = std::find(whitelisted_principals.begin(),
                      whitelisted_principals.end(), principal_id_caller);
  if (it != whitelisted_principals.end()) {
    std::cout << "User is whitelisted." << std::endl;
  } else {
    std::cout << "User is not whitelisted." << std::endl;
  }

  // This call expects as argument a string in the format of a Candid principal
  // Note that this is NOT the caller's principal ID, it is just an argument.
  // For example, if the caller wants to send some ICP to another principal,
  // the caller's principal id is authenticated as above, while the receiving
  // principal's id is passed in as argument:
  // - Value follows the rules for text representation of a Candid principal
  // - It's just a string, nothing more. Not authenticated by IC using keys
  std::string principal_id_from_arg{""};
  ic_api.from_wire(CandidTypePrincipal{&principal_id_from_arg});
  std::cout << "Principal id passed by argument is " + principal_id_from_arg
            << std::endl;

  // For QA purposes, send the principal id back to the wire
  ic_api.to_wire(CandidTypePrincipal{principal_id_from_arg});
}

void demo_candid_type_principals() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string in1{""};
  std::string in2{""};
  CandidArgs args_in;
  args_in.append(CandidTypePrincipal(&in1));
  args_in.append(CandidTypePrincipal(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" + in1 +
                   "' & '" + in2 + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypePrincipal(in1));
  args_out.append(CandidTypePrincipal(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_principal.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_principal()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_principal");
void demo_candid_type_principals()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_principals");
// file: src/demo.did
service : {
    "demo_candid_type_principal" : (principal) -> (principal) query;
    "demo_candid_type_principals" : (principal, principal) -> (principal, principal) query; 
};

CandidTypeRecord

A class to convert between Candid record and C++.

A CandidTypeRecord is a like a dictionary, constructed from multiple Candid types.

Declaration:

// candid.h
class CandidTypeRecord {
public:
  CandidTypeRecord();
  void append(uint32_t field_id, CandidType field);
  void append(std::string field_name, CandidType field);
  void append(CandidType field); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_record.cpp

$ icp canister call demo demo_candid_type_record '(record {"field 1" = true : bool; "field 2" = 0.1 : float32; "field 3" = 0.2 : float64; "field 4" = -8 : int8; "field 5" = -16 : int16; "field 6" = -32 : int32; "field 7" = -64 : int64; "field 8" = -128 : int; "field 9" = 8 : nat8; "field 10" = 16 : nat16; "field A" = 32 : nat32; "field B" = 64 : nat64; "field C" = 128 : nat; "field D" = principal "2ibo7-dia"; "field E" = "demo" : text;})' --environment local
(record { field 10 = 16 : nat16; field 1 = true; field 2 = 0.1 : float32; field 3 = 0.2 : float64; field 4 = -8 : int8; field 5 = -16 : int16; field 6 = -32 : int32; field 7 = -64 : int64; field 8 = -128 : int; field 9 = 8 : nat8; field A = 32 : nat32; field B = 64 : nat64; field C = 128 : nat; field D = principal "2ibo7-dia"; field E = "demo";})
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_record
 received a record with fields: 

 field 1  - bool        value '1
 field 2  - float       value '0.100000
 field 3  - double      value '0.200000
 field 4  - int8_t      value '-8
 field 5  - int16_t     value '-16
 field 6  - int32_t     value '-32
 field 7  - int64_t     value '-64
 field 8  - __int128_t  value '-128
 field 9  - uint8_t     value '8
 field 10 - uint16_t    value '16
 field A  - uint32_t    value '32
 field B  - uint64_t    value '64
 field C  - __uint128_t value '128
 field D  - std::string value '2ibo7-dia
 field E  - std::string value 'demo'

$ icp canister call demo demo_candid_type_records '(record {"field 1A" = -8 : int8;}, record {"field 2A" = -16 : int16;})' --environment local
(record { field 1A = -8 : int8;}, record { field 2A = -16 : int16;})
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_records
 received a record with field:
 field 1A  - int8_t      value '-8
 and      a record with field:
 field 1B  - int16_t     value '-16'

*/
#include "demo_candid_type_record.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_record() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ---------------------------------------------------------------------------
  // Get the data from the wire
  bool f1{false};
  float f2{0.0};
  double f3{0.0};
  int8_t f4{0};
  int16_t f5{0};
  int32_t f6{0};
  int64_t f7{0};
  __int128_t f8{0};
  uint8_t f9{0};
  uint16_t f10{0};
  uint32_t fa{0};
  uint64_t fb{0};
  __uint128_t fc{0};
  std::string fd{""};
  std::string fe{""};

  CandidTypeRecord r_in;
  r_in.append("field 1", CandidTypeBool{&f1});
  r_in.append("field 2", CandidTypeFloat32{&f2});
  r_in.append("field 3", CandidTypeFloat64{&f3});
  r_in.append("field 4", CandidTypeInt8{&f4});
  r_in.append("field 5", CandidTypeInt16{&f5});
  r_in.append("field 6", CandidTypeInt32{&f6});
  r_in.append("field 7", CandidTypeInt64{&f7});
  r_in.append("field 8", CandidTypeInt{&f8});
  r_in.append("field 9", CandidTypeNat8{&f9});
  r_in.append("field 10", CandidTypeNat16{&f10});
  r_in.append("field A", CandidTypeNat32{&fa});
  r_in.append("field B", CandidTypeNat64{&fb});
  r_in.append("field C", CandidTypeNat{&fc});
  r_in.append("field D", CandidTypePrincipal{&fd});
  r_in.append("field E", CandidTypeText{&fe});
  ic_api.from_wire(r_in);

  // ---------------------------------------------------------------------------
  std::cout << "Method " + std::string(__func__) +
                   "\n received a record with fields: \n" +
                   "\n field 1  - bool        value '" + std::to_string(f1) +
                   "\n field 2  - float       value '" + std::to_string(f2) +
                   "\n field 3  - double      value '" + std::to_string(f3) +
                   "\n field 4  - int8_t      value '" + std::to_string(f4) +
                   "\n field 5  - int16_t     value '" + std::to_string(f5) +
                   "\n field 6  - int32_t     value '" + std::to_string(f6) +
                   "\n field 7  - int64_t     value '" + std::to_string(f7) +
                   "\n field 8  - __int128_t  value '" +
                   IC_API::to_string_128(f8) +
                   "\n field 9  - uint8_t     value '" + std::to_string(f9) +
                   "\n field 10 - uint16_t    value '" + std::to_string(f10) +
                   "\n field A  - uint32_t    value '" + std::to_string(fa) +
                   "\n field B  - uint64_t    value '" + std::to_string(fb) +
                   "\n field C  - __uint128_t value '" +
                   IC_API::to_string_128(fc) +
                   "\n field D  - std::string value '" + fd +
                   "\n field E  - std::string value '" + fe + "'"
            << std::endl;
  // ---------------------------------------------------------------------------

  CandidTypeRecord r_out;
  r_out.append("field 1", CandidTypeBool{f1});
  r_out.append("field 2", CandidTypeFloat32{f2});
  r_out.append("field 3", CandidTypeFloat64{f3});
  r_out.append("field 4", CandidTypeInt8{f4});
  r_out.append("field 5", CandidTypeInt16{f5});
  r_out.append("field 6", CandidTypeInt32{f6});
  r_out.append("field 7", CandidTypeInt64{f7});
  r_out.append("field 8", CandidTypeInt{f8});
  r_out.append("field 9", CandidTypeNat8{f9});
  r_out.append("field 10", CandidTypeNat16{f10});
  r_out.append("field A", CandidTypeNat32{fa});
  r_out.append("field B", CandidTypeNat64{fb});
  r_out.append("field C", CandidTypeNat{fc});
  r_out.append("field D", CandidTypePrincipal{fd});
  r_out.append("field E", CandidTypeText{fe});
  ic_api.to_wire(r_out);
}

void demo_candid_type_records() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ---------------------------------------------------------------------------
  // Get the data from the wire
  int8_t f1a{0};
  CandidTypeRecord r1_in;
  r1_in.append("field 1A", CandidTypeInt8{&f1a});

  int16_t f2a{0};
  CandidTypeRecord r2_in;
  r2_in.append("field 2A", CandidTypeInt16{&f2a});

  CandidArgs args_in;
  args_in.append(r1_in);
  args_in.append(r2_in);
  ic_api.from_wire(args_in);

  // ---------------------------------------------------------------------------
  std::cout << "Method " + std::string(__func__) +
                   "\n received a record with field:" +
                   "\n field 1A  - int8_t      value '" + std::to_string(f1a) +
                   "\n and      a record with field:" +
                   "\n field 2A  - int16_t     value '" + std::to_string(f2a) +
                   "'"
            << std::endl;

  // ---------------------------------------------------------------------------
  // Return the data
  CandidTypeRecord r1_out;
  r1_out.append("field 1A", CandidTypeInt8{f1a});

  CandidTypeRecord r2_out;
  r2_out.append("field 2A", CandidTypeInt16{f2a});

  CandidArgs args_out;
  args_out.append(r1_out);
  args_out.append(r2_out);
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_record.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_record()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_record");
void demo_candid_type_records()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_records");
// file: src/demo.did
service : {
    "demo_candid_type_record" : (record {"field 1" : bool; "field 2" : float32; "field 3" : float64; "field 4" : int8; "field 5" : int16; "field 6" : int32; "field 7": int64; "field 8" : int; "field 9" : nat8; "field 10" : nat16; "field A" : nat32; "field B" : nat64; "field C" : nat; "field D" : principal; "field E" : text;})   -> 
                                (record {"field 1" : bool; "field 2" : float32; "field 3" : float64; "field 4" : int8; "field 5" : int16; "field 6" : int32; "field 7": int64; "field 8" : int; "field 9" : nat8; "field 10" : nat16; "field A" : nat32; "field B" : nat64; "field C" : nat; "field D" : principal; "field E" : text;}) 
                                query;
    "demo_candid_type_records" : (record {"field 1A" : int8;}, record {"field 2A" : int16;}) -> 
                                 (record {"field 1A" : int8;}, record {"field 2A" : int16;}) 
                                 query; 
};

CandidTypeText

A class to convert between Candid text and C++ std::string.

Declaration:

// candid.h
class CandidTypeText {
public:
  CandidTypeText(std::string *v);
  CandidTypeText(const std::string v); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_text.cpp

$ icp canister call demo demo_candid_type_text '("demo A" : text)' --environment local
("demo A")
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_text received value 'demo A'

$ icp canister call demo demo_candid_type_texts '("demo A" : text, "demo B" : text)' --environment local
("demo A", "demo B")
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_texts received values 'demo A' & 'demo B'

*/
#include "demo_candid_type_text.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_text() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string in{""};
  ic_api.from_wire(CandidTypeText{&in});
  std::cout << "Method " + std::string(__func__) + " received value '" + in +
                   "'"
            << std::endl;

  ic_api.to_wire(CandidTypeText{in});
}

void demo_candid_type_texts() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string in1{""};
  std::string in2{""};
  CandidArgs args_in;
  args_in.append(CandidTypeText(&in1));
  args_in.append(CandidTypeText(&in2));
  ic_api.from_wire(args_in);

  std::cout << "Method " + std::string(__func__) + " received values '" + in1 +
                   "' & '" + in2 + "'"
            << std::endl;

  CandidArgs args_out;
  args_out.append(CandidTypeText(in1));
  args_out.append(CandidTypeText(in2));
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_text.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_text()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_text");
void demo_candid_type_texts()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_texts");
// file: src/demo.did
service : {
    "demo_candid_type_text" : (text) -> (text) query;
    "demo_candid_type_texts" : (text, text) -> (text, text) query; 
};

CandidTypeVariant

A class to convert between Candid variant and C++.

A CandidTypeVariant is similar to the CandidTypeRecord, but it can only store one 'label: value' out of multiple allowed variants.

Declaration:

// candid.h
class CandidTypeVariant {
public:
  CandidTypeVariant();
  CandidTypeVariant(std::string *p_label);
  CandidTypeVariant(const std::string label);
  CandidTypeVariant(const std::string label, const CandidType field); 
}

Example:

/* file: https://github.com/icppWorld/icpp-demos/tree/main/canisters/api_reference/src/demo_candid_type_variant.cpp

$ icp canister call demo demo_candid_type_variant '(variant {"field 2" = 0.1 : float32;})' --environment local
(variant { field 2 = 0.1 : float32;})
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_variant
    received a variant with label: field 2

    field 2  - float       value '0.100000

$ icp canister call demo demo_candid_type_variants '(variant {"field 1" = true : bool;}, variant {"field 2" = 0.1 : float32;})' --environment local
(variant {"field 1" = true : bool;}, variant {"field 2" = 0.1 : float32;})
-> check the console of the local network. The canister will print:
   [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] Method demo_candid_type_variants

    received a variant with label: field 2

    field 2  - float       value '0.100000

    received another variant with label: field 2

    field 2  - float       value '0.100000

*/
#include "demo_candid_type_variant.h"

#include <iostream>
#include <string>

#include "ic_api.h"

void demo_candid_type_variant() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ---------------------------------------------------------------------------
  // Get the data from the wire
  bool f1{false};
  float f2{0.0};
  double f3{0.0};
  int8_t f4{0};
  int16_t f5{0};
  int32_t f6{0};
  int64_t f7{0};
  __int128_t f8{0};
  uint8_t f9{0};
  uint16_t f10{0};
  uint32_t fa{0};
  uint64_t fb{0};
  __uint128_t fc{0};
  std::string fd{""};
  std::string fe{""};

  std::string label{""};
  CandidTypeVariant v_in(&label);
  v_in.append("field 1", CandidTypeBool{&f1});
  v_in.append("field 2", CandidTypeFloat32{&f2});
  v_in.append("field 3", CandidTypeFloat64{&f3});
  v_in.append("field 4", CandidTypeInt8{&f4});
  v_in.append("field 5", CandidTypeInt16{&f5});
  v_in.append("field 6", CandidTypeInt32{&f6});
  v_in.append("field 7", CandidTypeInt64{&f7});
  v_in.append("field 8", CandidTypeInt{&f8});
  v_in.append("field 9", CandidTypeNat8{&f9});
  v_in.append("field 10", CandidTypeNat16{&f10});
  v_in.append("field A", CandidTypeNat32{&fa});
  v_in.append("field B", CandidTypeNat64{&fb});
  v_in.append("field C", CandidTypeNat{&fc});
  v_in.append("field D", CandidTypePrincipal{&fd});
  v_in.append("field E", CandidTypeText{&fe});
  ic_api.from_wire(v_in);

  // ---------------------------------------------------------------------------
  // Debug print of the values

  std::string msg;
  msg.append("Method " + std::string(__func__) +
             "\n received a variant with label: " + label + "\n");

  if (label == "field 1") {
    msg.append("\n field 1  - bool        value '" + std::to_string(f1));
  } else if (label == "field 2") {
    msg.append("\n field 2  - float       value '" + std::to_string(f2));
  } else if (label == "field 3") {
    msg.append("\n field 3  - double      value '" + std::to_string(f3));
  } else if (label == "field 4") {
    msg.append("\n field 4  - int8_t      value '" + std::to_string(f4));
  } else if (label == "field 5") {
    msg.append("\n field 5  - int16_t     value '" + std::to_string(f5));
  } else if (label == "field 6") {
    msg.append("\n field 6  - int32_t     value '" + std::to_string(f6));
  } else if (label == "field 7") {
    msg.append("\n field 7  - int64_t     value '" + std::to_string(f7));
  } else if (label == "field 8") {
    msg.append("\n field 8  - __int128_t  value '" + IC_API::to_string_128(f8));
  } else if (label == "field 9") {
    msg.append("\n field 9  - uint8_t     value '" + std::to_string(f9));
  } else if (label == "field 10") {
    msg.append("\n field 10 - uint16_t    value '" + std::to_string(f10));
  } else if (label == "field A") {
    msg.append("\n field A  - uint32_t    value '" + std::to_string(fa));
  } else if (label == "field B") {
    msg.append("\n field B  - uint64_t    value '" + std::to_string(fb));
  } else if (label == "field C") {
    msg.append("\n field C  - __uint128_t value '" + IC_API::to_string_128(fc));
  } else if (label == "field D") {
    msg.append("\n field D  - std::string value '" + fd);
  } else if (label == "field E") {
    msg.append("\n field E  - std::string value '" + fe);
  }

  std::cout << msg << std::endl;

  // ---------------------------------------------------------------------------
  // Return the data
  CandidTypeVariant v_out{label};
  if (label == "field 1") {
    v_out.append("field 1", CandidTypeBool{f1});
  } else if (label == "field 2") {
    v_out.append("field 2", CandidTypeFloat32{f2});
  } else if (label == "field 3") {
    v_out.append("field 3", CandidTypeFloat64{f3});
  } else if (label == "field 4") {
    v_out.append("field 4", CandidTypeInt8{f4});
  } else if (label == "field 5") {
    v_out.append("field 5", CandidTypeInt16{f5});
  } else if (label == "field 6") {
    v_out.append("field 6", CandidTypeInt32{f6});
  } else if (label == "field 7") {
    v_out.append("field 7", CandidTypeInt64{f7});
  } else if (label == "field 8") {
    v_out.append("field 8", CandidTypeInt{f8});
  } else if (label == "field 9") {
    v_out.append("field 9", CandidTypeNat8{f9});
  } else if (label == "field 10") {
    v_out.append("field 10", CandidTypeNat16{f10});
  } else if (label == "field A") {
    v_out.append("field A", CandidTypeNat32{fa});
  } else if (label == "field B") {
    v_out.append("field B", CandidTypeNat64{fb});
  } else if (label == "field C") {
    v_out.append("field C", CandidTypeNat{fc});
  } else if (label == "field D") {
    v_out.append("field D", CandidTypePrincipal{fd});
  } else if (label == "field E") {
    v_out.append("field E", CandidTypeText{fe});
  }
  ic_api.to_wire(v_out);
}

void demo_candid_type_variants() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ---------------------------------------------------------------------------
  // Get the data from the wire for two variants

  // variant a
  bool f1a{false};
  float f2a{0.0};

  std::string labela{""};
  CandidTypeVariant va_in(&labela);
  va_in.append("field 1", CandidTypeBool{&f1a});
  va_in.append("field 2", CandidTypeFloat32{&f2a});

  // variant b
  bool f1b{false};
  float f2b{0.0};

  std::string labelb{""};
  CandidTypeVariant vb_in(&labelb);
  vb_in.append("field 1", CandidTypeBool{&f1b});
  vb_in.append("field 2", CandidTypeFloat32{&f2b});

  CandidArgs args_in;
  args_in.append(va_in);
  args_in.append(vb_in);
  ic_api.from_wire(args_in);

  // ---------------------------------------------------------------------------
  // Debug print of the values

  // variant a
  std::string msg;
  msg.append("Method " + std::string(__func__) +
             "\n received a variant with label: " + labela + "\n");

  if (labela == "field 1") {
    msg.append("\n field 1  - bool        value '" + std::to_string(f1a));
  } else if (labela == "field 2") {
    msg.append("\n field 2  - float       value '" + std::to_string(f2a));
  }

  // variant b
  msg.append("\n\n received another variant with label: " + labelb + "\n");

  if (labelb == "field 1") {
    msg.append("\n field 1  - bool        value '" + std::to_string(f1b));
  } else if (labelb == "field 2") {
    msg.append("\n field 2  - float       value '" + std::to_string(f2b));
  }

  std::cout << msg << std::endl;

  // ---------------------------------------------------------------------------
  // Return the data

  // variant a
  CandidTypeVariant va_out{labela};
  if (labela == "field 1") {
    va_out.append("field 1", CandidTypeBool{f1a});
  } else if (labela == "field 2") {
    va_out.append("field 2", CandidTypeFloat32{f2a});
  }

  // variant b
  CandidTypeVariant vb_out{labelb};
  if (labelb == "field 1") {
    vb_out.append("field 1", CandidTypeBool{f1b});
  } else if (labelb == "field 2") {
    vb_out.append("field 2", CandidTypeFloat32{f2b});
  }

  CandidArgs args_out;
  args_out.append(va_out);
  args_out.append(vb_out);
  ic_api.to_wire(args_out);
}
// file: src/demo_candid_type_variant.h
#pragma once
#include "wasm_symbol.h"
void demo_candid_type_variant()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_variant");
void demo_candid_type_variants()
    WASM_SYMBOL_EXPORTED("canister_query demo_candid_type_variants");
// file: src/demo.did
service : {
    "demo_candid_type_variant" : (variant {"field 1" : bool; "field 2" : float32; "field 3" : float64; "field 4" : int8; "field 5" : int16; "field 6" : int32; "field 7": int64; "field 8" : int; "field 9" : nat8; "field 10" : nat16; "field A" : nat32; "field B" : nat64; "field C" : nat; "field D" : principal; "field E" : text;})   -> 
                                 (variant {"field 1" : bool; "field 2" : float32; "field 3" : float64; "field 4" : int8; "field 5" : int16; "field 6" : int32; "field 7": int64; "field 8" : int; "field 9" : nat8; "field 10" : nat16; "field A" : nat32; "field B" : nat64; "field C" : nat; "field D" : principal; "field E" : text;}) 
                                 query;
    "demo_candid_type_variants" : (variant {"field 1" : bool; "field 2" : float32;}, variant {"field 1" : bool; "field 2" : float32;}) -> 
                                  (variant {"field 1" : bool; "field 2" : float32;}, variant {"field 1" : bool; "field 2" : float32;})
                                  query; 
};

CandidTypeVec

A group of classes to convert between Candid vec candid-type and C++ std::vector<C++ type>.

CandidTypeVec Candid C++ Example
CandidTypeVecBool vec bool std::vector<bool>
not supported vec empty not supported
CandidTypeVecFloat32 vec float32 std::vector<float>
CandidTypeVecFloat64 vec float64 std::vector<double>
CandidTypeVecInt8 vec int8 std::vector<int8_t>
CandidTypeVecInt16 vec int16 std::vector<int16_t>
CandidTypeVecInt32 vec int32 std::vector<int32_t>
CandidTypeVecInt64 vec int64 std::vector<int64_t>
CandidTypeVecInt vec int std::vector<__int128_t>
CandidTypeVecNat8 vec nat8 std::vector<uint8_t>
CandidTypeVecNat16 vec nat16 std::vector<uint16_t>
CandidTypeVecNat32 vec nat32 std::vector<uint32_t>
CandidTypeVecNat64 vec nat64 std::vector<uint64_t>
CandidTypeVecNat vec nat std::vector<__uint128_t>
not supported vec null not supported
CandidTypeVecPrincipal vec principal std::vector<std::string>
CandidTypeVecText vec text std::vector<std::string>
CandidTypeVecBlob vec blob std::vector<std::vector<uint8_t>>

vec blob (a vec of vec nat8) is used for example for the derivation paths of threshold signatures. Other vecs of vecs are not supported.

CandidTypeFunc

A reference to a method of a canister, func, for encoding only: for example the transform query of an HTTPS outcall, which IC_Management::http_request builds for you.

CandidTypeFunc transform{ic_api.get_canister_self(), "transform"};
transform.append_arg(transform_args_type); // the types of the arguments
transform.append_ret(result_type);         // the types of the results
transform.set_query();                     // or set_composite_query(), set_oneway()

🚧 This documentation section is currently under construction! 🚧


IC_API

When developing a C++ Smart Contract for the Internet Computer, you engage with the canister system API using the IC_API. Among other things, it facilitates:

  • Verifying the identity of the caller
  • Acquiring incoming data from the wire
  • Dispatching data back to the wire
  • Printing to the console, when deployed to the local network
  • Getting the Internet Computer system time
  • Trapping a canister to stop execution and return an error

The message data is organized in the Candid format, and the IC_API seamlessly aligns this with standard C++ data structures.


ic0 system interface

The methods detailed in this section encapsulate the functionality of the ic0 system interface.

As a C++ developer, you can interact with the Internet Computer Interface seamlessly, without delving into its intricacies, thanks to the abstraction provided by icpp-pro's C++ interface methods.


debug_print

Declaration:

// ic_api.h
class IC_API {
public:
  static void debug_print(const char *msg);
  static void debug_print(const std::string &msg); 
}

The debug_print method is a utility function used to print debug messages during the local execution of your application. This is helpful for tracing and debugging your application's flow. The debug_print method accepts a C-style string (const char *) or a C++ string (const std::string &).

Note that the debug_print messages only appear during local development and testing. In a production environment on the IC mainnet, these messages are disregarded.

Example:

/* file: src/demo_debug_print.cpp

$ icp canister call demo demo_debug_print '()' --environment local
...nothing is printed here

-> check the console of the local network. The canister will print:
   Hello expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae

-> when deployed to IC's mainnet, nothing is printed.

*/
#include "demo_debug_print.h"
#include "ic_api.h"
#include <iostream>

void demo_debug_print() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // You can use this
  std::cout << "Hello " + caller.get_text() << std::endl;

  // Or this.
  IC_API::debug_print("Hello " + caller.get_text());

  ic_api.from_wire();
  ic_api.to_wire();
}
// file: src/demo_debug_print.h
#pragma once
#include "wasm_symbol.h"
void demo_debug_print() WASM_SYMBOL_EXPORTED("canister_query demo_debug_print");
// file: src/demo.did
service : {
    "demo_debug_print": () -> () query; 
};

In this example, the debug_print() method is used to display a greeting message containing the caller's principal. The demo_debug_print() method retrieves the CandidTypePrincipal of the caller using theget_caller() method, then it creates a message and passes it to debug_print().

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares one query function, demo_debug_print, accepting no arguments and returning no values.


IC_API constructor

Declaration:

// ic_api.h
class IC_API {
public:
  IC_API(const CanisterBase &canister_entry,
         const bool &debug_print); 
}

This constructor creates an IC_API instance and initializes the debug printing flag according to the debug_print parameter passed to it.

Example:

Here's an example of how to create an IC_API instance at the start of your public C++ method:

/* file: src/demo_ic_api.cpp

$ icp canister call demo demo_ic_api '()' --environment local

*/
#include "demo_ic_api.h"
#include "ic_api.h"

void demo_ic_api() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();
  ic_api.from_wire();
  ic_api.to_wire();
}
// file: src/demo_ic_api.h
#pragma once
#include "wasm_symbol.h"
void demo_ic_api() WASM_SYMBOL_EXPORTED("canister_query demo_ic_api");
// file: src/demo.did
service : {
    "demo_ic_api": () -> () query; 
};

In this example, an IC_API object named ic_api is created with the debug_print flag set to false. If the debug_print attribute is toggled to true, it results in more detailed console outputs during local execution. However, this flag is disregarded when the application operates in a production environment on the IC mainnet.

The demo_ic_api canister method does not expect an argument, and does not return an argument. This is expressed by the empty argument lists for the from_wire & to_wire methods.

The from_wire method is not required, but recommended, because it will verify that the incoming message indeed has zero arguments, and if not, it will trap explicitly and reject the message.

The to_wire method is required to return the response. If omitted, the canister will will return Error: Failed query call, with message that the canister did not reply to the call.

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares one query function, demo_ic_api, accepting no arguments and returning no values.


get_caller

Declaration:

// ic_api.h
class IC_API {
public:
  CandidTypePrincipal get_caller(); 
}

This method returns the principal of the caller. The return type is CandidTypePrincipal.

In the Internet Computer, principals are unique identifiers. They are derived from a public key and are cryptographically secure. When a user (or another canister) calls a canister method, they sign the message with their private key. The Internet Computer then uses the corresponding public key to verify the signature and thus the authenticity of the call.

Example:

Here is how to use get_caller() in your C++ method:

/* file: src/demo_get_caller.cpp

$ icp canister call demo demo_get_caller '()' --environment local
(Hello! Your principal is expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae)

*/
#include "demo_get_caller.h"
#include "ic_api.h"

void demo_get_caller() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  // The caller's authenticated principal
  // - The calling message was signed using the caller's private key
  // - The IC verified the signature using the corresponding public key
  CandidTypePrincipal caller = ic_api.get_caller();

  ic_api.from_wire();

  std::string msg;
  msg.append("Hello! Your principal is ");
  msg.append(caller.get_text());

  ic_api.to_wire(CandidTypeText(msg));
}
// file: src/demo_get_caller.h
#pragma once
#include "wasm_symbol.h"
void demo_get_caller() WASM_SYMBOL_EXPORTED("canister_query demo_get_caller");
// file: src/demo.did
service : {
    "demo_get_caller": () -> (text) query; 
};

In this example, get_caller() is used to retrieve the CandidTypePrincipal representing the caller. This could be useful in methods where you need to identify or verify the caller.

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares one query function, demo_get_caller, accepting no arguments and returning a Candid text value.


get_canister_self

🚧 This documentation section is currently under construction! 🚧

Returns the principal (id) of the canister.

See QA canister_1 for an example.


get_canister_self_cycle_balance

🚧 This documentation section is currently under construction! 🚧

Returns the cycle balance of the canister.

See QA canister_1 for an example.


from_wire

Declaration:

// ic_api.h
class IC_API {
public:
  void from_wire();
  void from_wire(CandidType arg_in);
  void from_wire(CandidArgs args_in);
  void from_wire(IC_HttpRequest &request);
  void from_wire(IC_HttpUpdateRequest &request); 
}

Receives data from the wire in Candid format, assuming either no arguments, a single argument of a CandidType object, or a CandidArgs object to receive multiple arguments.

C++ data structures are passed into the CandidType objects as pointers, and the from_wire will deserialize the Candid byte stream and map the values on them.

Example:

Here is how to use from_wire() in your C++ method:

/* file: src/demo_from_wire.cpp

$ icp canister call demo demo_from_wire_no_arg '()' --environment local


$ icp canister call demo demo_from_wire_one_arg '("Neuron Staking")' --environment local
("Hello expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae, your hobby is Neuron Staking")

$ icp canister call demo demo_from_wire_multiple_args '("Neuron Staking", 3000 : nat64)' --environment local
("Hello expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae, you earned 3000 ICP from your hobby, Neuron Staking")

*/
#include "demo_from_wire.h"
#include "ic_api.h"

void demo_from_wire_no_arg() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  ic_api.from_wire();
  ic_api.to_wire();
}

void demo_from_wire_one_arg() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string hobby{""};
  ic_api.from_wire(CandidTypeText{&hobby});

  std::string msg;
  msg.append("Hello " + caller.get_text() + ", ");
  msg.append("your hobby is " + hobby);

  ic_api.to_wire(CandidTypeText(msg));
}

void demo_from_wire_multiple_args() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string hobby{""};
  uint64_t num_icp{0};
  CandidArgs args_in;
  args_in.append(CandidTypeText(&hobby));
  args_in.append(CandidTypeNat64(&num_icp));
  ic_api.from_wire(args_in);

  std::string msg;
  msg.append("Hello " + caller.get_text() + ", ");
  msg.append("you earned " + std::to_string(num_icp) +
             " ICP from your hobby, " + hobby);
  ic_api.to_wire(CandidTypeText(msg));
}
// file: src/demo_from_wire.h
#pragma once
#include "wasm_symbol.h"
void demo_from_wire_no_arg()
    WASM_SYMBOL_EXPORTED("canister_query demo_from_wire_no_arg");
void demo_from_wire_one_arg()
    WASM_SYMBOL_EXPORTED("canister_query demo_from_wire_one_arg");
void demo_from_wire_multiple_args()
    WASM_SYMBOL_EXPORTED("canister_query demo_from_wire_multiple_args");
// file: src/demo.did
service : {
    "demo_from_wire_no_arg": () -> () query;
    "demo_from_wire_one_arg": (text) -> (text) query;
    "demo_from_wire_multiple_args": (text, nat64) -> (text) query; 
};

In this example, demo_from_wire() is used to receive and process data in various scenarios - with no arguments, with one argument, and with multiple arguments.

The demo_from_wire_no_arg() function simply creates an IC_API instance, calls from_wire() without any arguments, and then calls to_wire(). This is a basic use case where no data is received or sent over the wire.

The demo_from_wire_one_arg() function, on the other hand, receives a single CandidTypeText object. The from_wire() function is called with this object, which then deserializes the incoming Candid byte stream and maps the value onto the hobby string. After creating a response message using the received hobby and the caller's principal, it calls to_wire() to send the response.

Lastly, the demo_from_wire_multiple_args() function handles a scenario where multiple arguments are received. It defines a CandidArgs object, appends the expected arguments to that, and calls from_wire() with this object. The function then constructs a response using the received hobby and the number of ICP earned from it, and calls to_wire() to send this response.

This demonstrates how from_wire() can be used effectively to receive and process data in a variety of use cases in your C++ methods.

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares the three query functions and specifies the Candid types each functions accepts as arguments and returns as values.


is_controller

🚧 This documentation section is currently under construction! 🚧

Returns true if the principal passed as argument is a controller of the canister.

See QA canister_1 for an example.


time

Declaration:

// ic_api.h
class IC_API {
public:
  static uint64_t time(); 
}

The time method is used to retrieve the current system time from the Internet Computer. The time is returned as a uint64_t representing the number of nanoseconds since the Unix epoch (1970-01-01). The time, as observed by the canister, is guaranteed to be monotonically increasing, even across canister upgrades. Additionally, within a single invocation of one entry point, the time is constant.

Please note that due to the decentralized nature of the Internet Computer, the system times of different canisters are unrelated and there is no notion of a timezone. Hence, calls from one canister to another may appear to travel “backwards in time”.

Example:

Here is how to use time() in your C++ method:

/* file: src/demo_time.cpp

$ icp canister call demo demo_time '()' --environment local
("The current system time in nanoseconds: 1684265399218380314 (2023-5-16 19:29:59)")

*/
#include "demo_time.h"
#include "ic_api.h"
#include <ctime>
#include <iostream>
#include <sstream>

std::string format_time(uint64_t time_in_ns) {
  /* 
  https://internetcomputer.org/docs/current/references/ic-interface-spec#system-api-time
  The time is given as nanoseconds since 1970-01-01. 
  - The IC guarantees that the time, as observed by the canister, is monotonically increasing, 
    even across canister upgrades. 
  - Within an invocation of one entry point, the time is constant. 
  - The system times of different canisters are unrelated, and calls from one canister to another 
    may appear to travel “backwards in time”.
  - The IC is decentralized and has no notion of a timezone.
  */
  std::time_t time_in_s = time_in_ns / 1'000'000'000;
  std::tm *tm = std::gmtime(&time_in_s);

  // Format the time manually
  std::string formatted_time = "";
  formatted_time += std::to_string(tm->tm_year + 1900); // Year
  formatted_time += "-";
  formatted_time += std::to_string(tm->tm_mon + 1); // Month
  formatted_time += "-";
  formatted_time += std::to_string(tm->tm_mday); // Day
  formatted_time += " ";
  formatted_time += std::to_string(tm->tm_hour); // Hour
  formatted_time += ":";
  formatted_time += std::to_string(tm->tm_min); // Minute
  formatted_time += ":";
  formatted_time += std::to_string(tm->tm_sec); // Second

  return formatted_time;
}

void demo_time() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  uint64_t time_in_ns = ic_api.time();

  std::string msg;
  msg.append("The current system time in nanoseconds: " +
             std::to_string(time_in_ns) + " (" + format_time(time_in_ns) + ")");

  std::cout << msg << std::endl;
  ic_api.to_wire(CandidTypeText(msg));
}
// file: src/demo_time.h
#pragma once
#include "wasm_symbol.h"
void demo_time() WASM_SYMBOL_EXPORTED("canister_query demo_time");
// file: src/demo.did
service : {
    "demo_time": () -> (text) query; rs

    // docs start: demo_to_wire
    "demo_to_wire_no_arg": () -> () query;
    "demo_to_wire_one_arg": (text) -> (text) query;
    "demo_to_wire_multiple_args": (text, nat64) -> (text, text, nat64, text) query; // docs end: demo_to_wire

    // docs start: demo_trap
    "demo_trap": () -> () query; // docs end: demo_trap

    // ---------------------------------------------------------------------------------------------
    // IC_API - utilities

    // docs start: demo_string_to_int128
    "demo_string_to_int128": () -> () query; // docs end: demo_string_to_int128

    // docs start: demo_to_string_128
    "demo_to_string_128": () -> () query; // docs end: demo_to_string_128

    // ---------------------------------------------------------------------------------------------
};


};

In this example, the time() method is used to retrieve the current system time in nanoseconds. The demo_time() method formats this time into a human-readable string and sends it back as a response to the caller.

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares one query function, demo_time, accepting no arguments and returning a Candid text value.


timers

Declaration:

// ic_api.h
class IC_API {
public:
  // The timer methods trap in query, inspect_message & canister_start
  // entry points - ic0.global_timer_set is refused there by the IC. The
  // guard uses the entry recorded by the live IC_API instance (cleared
  // when it destructs); while no IC_API is alive the guard is skipped.
  static uint64_t set_timer(uint64_t delay_ns, std::function<void()> cb);
  static uint64_t set_timer_recurring(uint64_t period_ns,
                                      std::function<void()> cb);
  static bool cancel_timer(uint64_t id); 

  // Cancels every currently registered timer (one-shot and recurring) and
  // disarms the IC's global timer.
  //
  // Valid entry points: canister_init, canister_post_upgrade,
  // canister_pre_upgrade, canister_update, canister_heartbeat,
  // canister_global_timer, and reply / reject / cleanup callbacks. Not
  // valid from canister_query, canister_inspect_message or canister_start
  // — ic0.global_timer_set is refused in those contexts per the IC
  // interface spec (and query mutations would be discarded anyway); the
  // timer methods enforce this with a clear trap. Caveat: a reply/reject
  // callback executing within a composite query is also refused by the IC,
  // which the entry model cannot detect - that case traps on the replica.
  //
  // Current-dispatch semantics: if called from inside a timer callback,
  // any due timer in the same dispatch batch that has NOT yet executed
  // is also cancelled — dispatch_due() looks each id up in the registry
  // at execute time and skips entries that have been removed. (Recurring
  // timers had their next deadline rescheduled during the batch's gather
  // phase; cancel_all_timers() removes them too, so they don't fire then
  // or later.) The currently-executing callback is not interrupted and
  // any callbacks that have already returned in this batch keep their
  // observable effects. The IC's global timer is disarmed and stays
  // disarmed until something else calls a set_timer* API.
  //
  // Raw-call interaction: users can still call ic0_global_timer_set
  // directly via #include "ic0.h". cancel_all_timers() only clears the
  // IcTimers registry and disarms the IC at the moment it runs — a
  // subsequent direct ic0_global_timer_set(t) call from user code can
  // re-arm the IC outside the registry's knowledge. Mixing the two is
  // the user's responsibility; the registry's correctness boundary does
  // not extend across that fence.
  static void cancel_all_timers(); 
}

The Internet Computer exposes a single global timer per canister, settable via ic0.global_timer_set and dispatched into a canister_global_timer export. icpp-pro layers a multi-timer scheduler on top so canister code can register many one-shot and recurring callbacks, each identified by a non-zero uint64_t id.

set_timer(delay_ns, cb) registers a one-shot callback that fires once after delay_ns nanoseconds. set_timer_recurring(period_ns, cb) registers a recurring callback that fires every period_ns nanoseconds. Both return the registry id you can later pass to cancel_timer(id), which returns true when an entry was actually removed and false if the id was unknown. cancel_all_timers() clears every registered entry and disarms the IC's global timer.

icpp-pro auto-exports a default canister_global_timer dispatcher (defined with __attribute__((weak))) that drains all due timers from the registry on each IC firing and re-arms the global timer for the next-due callback. If you need custom dispatch logic, provide your own strong definition of canister_global_timer to override it — see the icpp-pro canister_user_timer_override test canister for the pattern.

Timer state lives in canister memory only — it does not persist across upgrades. Re-register your timers in canister_post_upgrade if you need them to survive a redeploy. Within a single dispatch batch, callbacks not yet executed are skipped if a callback (or the IC) clears the registry mid-batch; this is the documented behavior of cancel_all_timers.

Example:

Here is how to use the multi-timer API in your C++ canister:

/* file: src/demo_timers.cpp

Demo of the icpp-pro multi-timer API:
  IC_API::set_timer(delay_ns, cb)            // one-shot, returns timer id
  IC_API::set_timer_recurring(period_ns, cb) // recurring, returns timer id
  IC_API::cancel_timer(id)                   // cancel one, returns bool
  IC_API::cancel_all_timers()                // cancel all + disarm

The IC platform exposes a single global timer per canister; icpp-pro layers
a multi-timer scheduler on top and auto-exports a `canister_global_timer`
dispatcher (marked weak so users can override it). Timer state is in-memory
only — re-register your timers in `canister_post_upgrade` if needed.

Example session:

  $ icp canister call demo demo_set_timer '(0 : nat64)' --environment local
  (1 : nat64)

  # ... wait ~1s for the IC to fire canister_global_timer ...

  $ icp canister call demo demo_get_one_shot_fires '()' --environment local
  (1 : nat64)

  $ icp canister call demo demo_set_timer_recurring '(1_000_000_000 : nat64)' --environment local
  (2 : nat64)

  # ... wait a few seconds ...

  $ icp canister call demo demo_get_recurring_fires '()' --environment local
  (3 : nat64)

  $ icp canister call demo demo_cancel_timer '(2 : nat64)' --environment local
  (true)

  $ icp canister call demo demo_cancel_all_timers '()' --environment local
  ()

*/
#include "demo_timers.h"
#include "ic_api.h"

namespace {
// Counters exposed via the demo_get_*_fires query methods. The lambdas
// passed to set_timer / set_timer_recurring capture references to these
// (or, since they're file-scope statics, just reference them directly).
uint64_t g_one_shot_fires{0};
uint64_t g_recurring_fires{0};
} // namespace

void demo_set_timer() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  uint64_t delay_ns{0};
  ic_api.from_wire(CandidTypeNat64{&delay_ns});

  uint64_t id = IC_API::set_timer(delay_ns, []() { ++g_one_shot_fires; });

  ic_api.to_wire(CandidTypeNat64{id});
}

void demo_set_timer_recurring() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  uint64_t period_ns{0};
  ic_api.from_wire(CandidTypeNat64{&period_ns});

  uint64_t id =
      IC_API::set_timer_recurring(period_ns, []() { ++g_recurring_fires; });

  ic_api.to_wire(CandidTypeNat64{id});
}

void demo_cancel_timer() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  uint64_t id{0};
  ic_api.from_wire(CandidTypeNat64{&id});

  bool ok = IC_API::cancel_timer(id);

  ic_api.to_wire(CandidTypeBool{ok});
}

void demo_cancel_all_timers() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  ic_api.from_wire();

  IC_API::cancel_all_timers();

  ic_api.to_wire();
}

void demo_get_one_shot_fires() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  ic_api.from_wire();
  ic_api.to_wire(CandidTypeNat64{g_one_shot_fires});
}

void demo_get_recurring_fires() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  ic_api.from_wire();
  ic_api.to_wire(CandidTypeNat64{g_recurring_fires});
}

void demo_reset_timer_counters() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  ic_api.from_wire();

  g_one_shot_fires = 0;
  g_recurring_fires = 0;
  IC_API::cancel_all_timers();

  ic_api.to_wire();
}

// file: src/demo_timers.h
#pragma once
#include "wasm_symbol.h"

// Schedule a one-shot callback to fire after `delay_ns` nanoseconds.
// Returns the timer id (a non-zero nat64) you can later pass to cancel.
void demo_set_timer() WASM_SYMBOL_EXPORTED("canister_update demo_set_timer");

// Schedule a recurring callback to fire every `period_ns` nanoseconds.
// Returns the timer id.
void demo_set_timer_recurring()
    WASM_SYMBOL_EXPORTED("canister_update demo_set_timer_recurring");

// Cancel a previously registered timer by id. Returns true if the id was
// found and removed, false if it was unknown.
void demo_cancel_timer()
    WASM_SYMBOL_EXPORTED("canister_update demo_cancel_timer");

// Cancel every currently registered timer (one-shot and recurring) and
// disarm the IC's global timer.
void demo_cancel_all_timers()
    WASM_SYMBOL_EXPORTED("canister_update demo_cancel_all_timers");

// Query how many times the one-shot callback has fired so far.
void demo_get_one_shot_fires()
    WASM_SYMBOL_EXPORTED("canister_query demo_get_one_shot_fires");

// Query how many times the recurring callback has fired so far.
void demo_get_recurring_fires()
    WASM_SYMBOL_EXPORTED("canister_query demo_get_recurring_fires");

// Reset both counters and cancel all timers — keeps the demo re-runnable
// without redeploying the canister.
void demo_reset_timer_counters()
    WASM_SYMBOL_EXPORTED("canister_update demo_reset_timer_counters");

// file: src/demo.did
service : {
    "demo_set_timer": (nat64) -> (nat64);
    "demo_set_timer_recurring": (nat64) -> (nat64);
    "demo_cancel_timer": (nat64) -> (bool);
    "demo_cancel_all_timers": () -> ();
    "demo_get_one_shot_fires": () -> (nat64) query;
    "demo_get_recurring_fires": () -> (nat64) query;
    "demo_reset_timer_counters": () -> (); 
};

In this example, demo_set_timer registers a one-shot callback that increments a file-scope counter when it fires; demo_set_timer_recurring does the same for a recurring callback. The query methods demo_get_one_shot_fires and demo_get_recurring_fires expose those counters so a client can observe the IC firing the timers in real time. demo_cancel_timer cancels by id, demo_cancel_all_timers clears the registry, and demo_reset_timer_counters resets state so the demo is re-runnable without redeploying.

A typical interactive session: call demo_set_timer(0) to register a one-shot at delay 0, wait briefly for the IC to fire canister_global_timer, then call demo_get_one_shot_fires and observe the counter at 1. Repeat with demo_set_timer_recurring(1_000_000_000) (period 1s) to see demo_get_recurring_fires increment over successive calls until you cancel it.


msg cycles

Declaration:

// ic_api.h
class IC_API {
public:
  // Available in update, query, reply- & reject-callback entry points;
  // get_msg_cycles_refunded only in reply- & reject-callbacks.
  // Query nuance: the IC provides these only in REPLICATED query execution.
  // CanisterQuery cannot see the execution mode, so the guard admits it and
  // a non-replicated query call traps on the IC itself - excluding query
  // here would wrongly block the legitimate replicated case.
  __uint128_t get_msg_cycles_available();
  __uint128_t get_msg_cycles_refunded();
  // Accepts min(max_amount, available); returns the amount accepted.
  __uint128_t accept_msg_cycles(__uint128_t max_amount); 
}

These methods work with the 128-bit cycles attached to the current message. get_msg_cycles_available returns how many cycles the caller sent along; accept_msg_cycles(max_amount) moves up to max_amount of them into the canister's balance and returns the amount actually accepted (the minimum of max_amount and what is available); get_msg_cycles_refunded reports, inside a reply or reject callback, how many cycles came back from an inter-canister call.

Note that an ingress message from a user never carries cycles, so get_msg_cycles_available returns 0 there — cycles only arrive on canister-to-canister calls. The methods trap with a clear message when called from an entry point where the IC does not provide them (for example get_msg_cycles_refunded outside a reply/reject callback).

Example: see the test canister in icpp-pro, which exercises the full accept flow against the Mock IC.


burn_cycles

Declaration:

// ic_api.h
class IC_API {
public:
  // Burns up to `amount` cycles from the canister's own balance; returns the
  // amount actually burned. Not available in inspect_message.
  __uint128_t burn_cycles(__uint128_t amount); 
}

burn_cycles(amount) burns up to amount cycles from the canister's own balance and returns the amount actually burned — never more than the balance allows without freezing the canister.

Example:

/* file: src/demo_cycles.cpp

$ icp canister call demo demo_cycles_balances '()' --environment local
("Cycle balance: 3091662937500 total, 3091592937500 liquid (spendable)")

$ icp canister call demo demo_burn_cycles '()' --environment local
("Burned 1000000 cycles. New balance: 3091661937500")

*/
#include "demo_cycles.h"
#include "ic_api.h"

void demo_cycles_balances() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  // The total balance, and the part of it the canister can spend without
  // dropping below its freezing threshold.
  __uint128_t total = ic_api.get_canister_self_cycle_balance();
  __uint128_t liquid = ic_api.get_canister_liquid_cycle_balance();

  std::string msg;
  msg.append("Cycle balance: " + IC_API::to_string_128(total) + " total, " +
             IC_API::to_string_128(liquid) + " liquid (spendable)");
  ic_api.to_wire(CandidTypeText(msg));
}

void demo_burn_cycles() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);

  // Burns up to the requested amount from the canister's own balance and
  // returns how much was actually burned.
  __uint128_t burned = ic_api.burn_cycles(1000000);
  __uint128_t balance = ic_api.get_canister_self_cycle_balance();

  std::string msg;
  msg.append("Burned " + IC_API::to_string_128(burned) +
             " cycles. New balance: " + IC_API::to_string_128(balance));
  ic_api.to_wire(CandidTypeText(msg));
}

// file: src/demo_cycles.h
#pragma once
#include "wasm_symbol.h"
void demo_cycles_balances()
    WASM_SYMBOL_EXPORTED("canister_query demo_cycles_balances");
void demo_burn_cycles()
    WASM_SYMBOL_EXPORTED("canister_update demo_burn_cycles");

// file: src/demo.did
service : {
    "demo_cycles_balances": () -> (text) query;
    "demo_burn_cycles": () -> (text); 
};

liquid cycle balance

Declaration:

// ic_api.h
class IC_API {
public:
  // The cycles spendable without the canister dropping below its freezing
  // threshold (<= get_canister_self_cycle_balance()).
  __uint128_t
  get_canister_liquid_cycle_balance(); 
}

get_canister_liquid_cycle_balance returns the portion of the cycle balance the canister can actually spend without dropping below its freezing threshold. It is always less than or equal to get_canister_self_cycle_balance, and is the number to check before attaching cycles to a call or burning them. See the demo_cycles_balances method in the example above, which prints both balances side by side.


inspect_message

Declaration:

// ic_api.h
class IC_API {
public:
  std::string get_msg_method_name();
  void accept_message(); 
}

The IC calls a canister's canister_inspect_message entry point for every ingress update call, before execution. Inside it, get_msg_method_name returns the name of the method being called, and accept_message accepts the message — an inspected message that is not accepted is refused, and accept_message traps when called twice.

icpp-pro deliberately does not export canister_inspect_message for you: exporting it turns on ingress filtering, which must be the canister author's explicit choice. To opt in, declare the hook yourself in your canister's header, exactly like any other entry point:

void canister_inspect_message()
    WASM_SYMBOL_EXPORTED("canister_inspect_message");

Example: the canister_inspect test canister in icpp-pro accepts only methods whose name starts with allowed_ and refuses everything else.


certified data

Declaration:

// ic_api.h
class IC_API {
public:
  void set_certified_data(const std::vector<uint8_t> &data);
  bool data_certificate_present();
  std::vector<uint8_t> get_data_certificate(); 
}

set_certified_data certifies up to 32 bytes — typically the hash of your application data — from an update-like entry point. In a later query call, data_certificate_present reports whether a certificate is available (it only is in non-replicated query calls), and get_data_certificate returns a certificate that clients can verify against the IC root key, proving the certified value was set by this canister on this subnet.

Example:

/* file: src/demo_certified_data.cpp

$ icp canister call demo demo_certified_data_set '()' --environment local
("Certified 32 bytes (use the hash of your application data)")

$ icp canister call demo demo_get_data_certificate '()' --environment local
("The data certificate is present: 621 bytes")

*/
#include "demo_certified_data.h"
#include "ic_api.h"

#include <vector>

void demo_certified_data_set() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);

  // At most 32 bytes can be certified - typically the hash of your
  // application data. The certificate becomes available in query calls
  // after this update call completes.
  std::vector<uint8_t> data(32);
  for (size_t i = 0; i < data.size(); ++i) {
    data[i] = (uint8_t)i;
  }
  ic_api.set_certified_data(data);

  ic_api.to_wire(
      CandidTypeText("Certified 32 bytes (use the hash of your application "
                     "data)"));
}

void demo_get_data_certificate() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  // The certificate is only present in a non-replicated query call. A
  // client can verify it against the IC root key, proving the certified
  // data was set by this canister on this subnet.
  std::string msg;
  if (ic_api.data_certificate_present()) {
    std::vector<uint8_t> cert = ic_api.get_data_certificate();
    msg = "The data certificate is present: " + std::to_string(cert.size()) +
          " bytes";
  } else {
    msg = "No data certificate (not a non-replicated query call)";
  }
  ic_api.to_wire(CandidTypeText(msg));
}

// file: src/demo_certified_data.h
#pragma once
#include "wasm_symbol.h"
void demo_certified_data_set()
    WASM_SYMBOL_EXPORTED("canister_update demo_certified_data_set");
void demo_get_data_certificate()
    WASM_SYMBOL_EXPORTED("canister_query demo_get_data_certificate");

// file: src/demo.did
service : {
    "demo_certified_data_set": () -> (text);
    "demo_get_data_certificate": () -> (text) query; 
};

system introspection

Declaration:

// ic_api.h
class IC_API {
public:
  // Incremented on every code install, upgrade & settings change. Not
  // available in canister_start.
  uint64_t get_canister_version(); 

  // Not available in canister_start. In canister_post_upgrade, Stopped tells
  // that the canister was fully stopped before the upgrade.
  enum class CanisterStatus : uint32_t {
    Running = 1,
    Stopping = 2,
    Stopped = 3
  };
  CanisterStatus get_canister_status(); 

  // False in a query call & in inspect_message, which run on a single node.
  // Available in every entry point.
  static bool in_replicated_execution(); 

  // Instructions executed so far in the current message (Instructions), or
  // in the whole call context (CallContextInstructions). Available in every
  // entry point. The limit of one message is 40B instructions for the first
  // message of an update, and 38B in a reply or reject callback - e.g. after
  // co_await ic_api.yield_fresh_budget(): an instruction guard in a yield
  // loop needs a lower ceiling after a yield. The Mock IC cannot count wasm
  // instructions; it returns an increasing nanosecond count, so the
  // difference of two readings still profiles the code in between. A native
  // test can feed its own counter with mockIC.set_performance_counter.
  enum class PerformanceCounterType : uint32_t {
    Instructions = 0,
    CallContextInstructions = 1
  };
  static uint64_t performance_counter(
      PerformanceCounterType type = PerformanceCounterType::Instructions);


  // The subnet this canister runs on, and its number of nodes. Not available
  // in canister_start.
  CandidTypePrincipal get_subnet_self();
  uint32_t get_subnet_node_count(); 

  // The IC root public key (DER-encoded BLS12-381, 133 bytes on mainnet), to
  // verify certificates. Not available in a query call (non-replicated),
  // inspect_message or canister_start.
  std::vector<uint8_t> get_root_key(); 
}

These methods tell a canister about itself and where it runs:

  • get_canister_version is incremented by the IC on every code install, upgrade and settings change.
  • get_canister_status is Running, Stopping or Stopped. In canister_post_upgrade, Stopped confirms that the canister was fully stopped before the upgrade, so no calls were in flight.
  • in_replicated_execution is false in a query call and in canister_inspect_message, which run on a single node, and true everywhere else, including a query method that was called as an update.
  • performance_counter returns the instructions executed so far in the current message, or with CallContextInstructions in the whole call context. Read it twice and subtract to profile a piece of code. One message may execute 40B instructions in the first message of an update, and 38B in a reply or reject callback, for example after co_await ic_api.yield_fresh_budget(): the IC reserves 5% of the limit for the cleanup callback. An instruction guard in a yield loop therefore needs a lower ceiling after a yield. The Mock IC cannot count wasm instructions: natively it returns an increasing nanosecond count, which still works for comparing two readings. A native test of code that sizes its work by instructions can feed its own counter with mockIC.set_performance_counter([](uint32_t type) -> uint64_t { ... }), and turn it off again with mockIC.clear_performance_counter().
  • get_subnet_self and get_subnet_node_count identify the subnet and its size.
  • get_root_key returns the IC root public key, 133 bytes on mainnet, to verify certificates against.

get_root_key is not available in a query call, nor in canister_inspect_message, and none of the methods except in_replicated_execution and performance_counter are available in canister_start. Each traps with a clear message in an entry point where the IC does not provide it, and the Mock IC refuses the underlying ic0 call in the same places.

Example:

/* file: src/demo_introspection.cpp

$ icp canister call demo demo_introspection '()' --environment local
("Canister version 2, running, replicated execution, on subnet w5bul-v73ez-ptacz-wphit-tyxt7-saait-4di22-ws2wo-dtv4x-2rmts-7ae with 13 nodes. The IC root key has 133 bytes. 312114 instructions executed so far.")

$ icp canister call demo demo_introspection_query '()' --environment local --query
("Non-replicated execution. Caller info: 0 bytes, no signer.")

Without --query, icp sends the call as an update, so the query method
executes replicated:
$ icp canister call demo demo_introspection_query '()' --environment local
("Replicated execution. Caller info: 0 bytes, no signer.")

*/
#include "demo_introspection.h"
#include "ic_api.h"

#include <string>

void demo_introspection() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);

  std::string status;
  switch (ic_api.get_canister_status()) {
  case IC_API::CanisterStatus::Running:
    status = "running";
    break;
  case IC_API::CanisterStatus::Stopping:
    status = "stopping";
    break;
  case IC_API::CanisterStatus::Stopped:
    status = "stopped";
    break;
  }

  // An update call executes on every node of the subnet (replicated).
  std::string msg =
      "Canister version " + std::to_string(ic_api.get_canister_version()) +
      ", " + status + ", " +
      (IC_API::in_replicated_execution() ? "replicated" : "non-replicated") +
      " execution, on subnet " + ic_api.get_subnet_self().get_text() +
      " with " + std::to_string(ic_api.get_subnet_node_count()) +
      " nodes. The IC root key has " +
      std::to_string(ic_api.get_root_key().size()) + " bytes. " +
      std::to_string(IC_API::performance_counter()) +
      " instructions executed so far.";
  ic_api.to_wire(CandidTypeText(msg));
}

void demo_introspection_query() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  // A query call executes on a single node (non-replicated). The root key
  // is not available here.
  std::string msg =
      std::string(IC_API::in_replicated_execution() ? "Replicated"
                                                    : "Non-replicated") +
      " execution. Caller info: " +
      std::to_string(ic_api.get_caller_info_data().size()) + " bytes, " +
      (ic_api.get_caller_info_signer().has_value() ? "signed by a canister"
                                                   : "no signer") +
      ".";
  ic_api.to_wire(CandidTypeText(msg));
}

// file: src/demo_introspection.h
#pragma once
#include "wasm_symbol.h"
void demo_introspection()
    WASM_SYMBOL_EXPORTED("canister_update demo_introspection");
void demo_introspection_query()
    WASM_SYMBOL_EXPORTED("canister_query demo_introspection_query");

// file: src/demo.did
service : {
    "demo_introspection": () -> (text);
    "demo_introspection_query": () -> (text) query; 
};

caller info

Declaration:

// ic_api.h
class IC_API {
public:
  // Extra information about a caller authenticated by a canister signature,
  // and the canister that signed it. Empty data & no signer for every other
  // caller. Available in update, query, reply & reject callbacks, cleanup &
  // inspect_message.
  std::vector<uint8_t> get_caller_info_data();
  std::optional<CandidTypePrincipal>
  get_caller_info_signer(); 
}

A user can sign in with a canister signature: a signature issued by a canister, such as an identity provider, rather than by a key the user holds. For such a caller, the IC passes along extra information about the caller, and get_caller_info_data returns it, while get_caller_info_signer returns the canister that issued the signature. For every other caller, including another canister, the data is empty and there is no signer.

Both methods are available in update, query, reply & reject callbacks, cleanup and canister_inspect_message.

Example: demo_introspection_query in the system introspection example above.


environment variables

Declaration:

// ic_api.h
class IC_API {
public:
  // The canister's environment variables, set with the canister settings
  // (icp.yaml: settings.environment_variables). get_env_var returns no value
  // when the name is not set. Not available in canister_start. In a wasm
  // build, getenv() sees them too.
  static std::map<std::string, std::string> get_env_vars();
  static std::optional<std::string>
  get_env_var(const std::string &name); 
}

A canister can have environment variables, set with its canister settings. In icp.yaml, you set them under settings.environment_variables:

canisters:
  - name: demo
    settings:
      environment_variables:
        GREETING: "Hello from icp.yaml"

icp deploy applies them, and also sets a PUBLIC_CANISTER_ID:<name> variable for each canister of the project. To change one without a redeploy, use icp canister settings update <name> --add-environment-variable KEY=VALUE; the next message sees the new value.

  • get_env_vars returns all of them, sorted by name.
  • get_env_var returns the value of one, or no value when it is not set.

In a wasm build, getenv() reads the same variables, so C and C++ code that is configured through the environment works unchanged. Two things to know:

  • In a static constructor, which runs while the canister module is instantiated, before any entry point, the environment is empty and getenv() returns NULL. Read environment variables in an entry point instead, such as canister_init.
  • The environment is rebuilt for every message, so a change made with setenv() lasts for the current message only.

In a native build, getenv() reads the environment of your shell. IC_API::get_env_vars and IC_API::get_env_var read the Mock IC's variables, which you set with mockIC.set_env_var(name, value).

The methods are available in every entry point except canister_start.

Example:

/* file: src/demo_env_vars.cpp

GREETING is set in icp.yaml (settings.environment_variables); icp-cli adds
PUBLIC_CANISTER_ID:demo on deploy.

$ icp canister call demo demo_env_vars '()' --environment local --query
("GREETING=Hello from icp.yaml. PUBLIC_CANISTER_ID:demo=4caro-hl777-77775-aaaba-cai. getenv(\"GREETING\") returns \"Hello from icp.yaml\".")

*/
#include "demo_env_vars.h"
#include "ic_api.h"

#include <cstdlib>
#include <string>

void demo_env_vars() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  std::string msg;
  for (const auto &[name, value] : IC_API::get_env_vars()) {
    msg += name + "=" + value + ". ";
  }

  // getenv() reads the same variables
  const char *greeting = getenv("GREETING");
  msg += std::string("getenv(\"GREETING\") returns ") +
         (greeting ? "\"" + std::string(greeting) + "\"" : "NULL") + ".";
  ic_api.to_wire(CandidTypeText(msg));
}

// file: src/demo_env_vars.h
#pragma once
#include "wasm_symbol.h"
void demo_env_vars() WASM_SYMBOL_EXPORTED("canister_query demo_env_vars");

// file: src/demo.did
service : {
    "demo_env_vars": () -> (text) query; 
};

cycle costs

Declaration:

// ic_api.h
class IC_API {
public:
  // The cycles an operation costs right now, so a canister can attach or
  // reserve enough. cost_call is what a canister needs above its freezing
  // threshold to make the call, including reservations for the largest
  // response; the actual call can cost less, never more. cost_http_request
  // takes the summed byte length of url, headers, body & transform, and the
  // max_response_bytes of the request (2,000,000 when not set). Available in
  // every entry point.
  static __uint128_t cost_call(uint64_t method_name_size,
                               uint64_t payload_size);
  static __uint128_t cost_create_canister();
  static __uint128_t cost_http_request(uint64_t request_size,
                                       uint64_t max_response_bytes);

  // The cost of an HTTPS outcall under pay-as-you-go pricing (the request's
  // pricing_version 2), from the resources it uses. outcall_type is how it is
  // replicated: on all nodes of the subnet, on one node (is_replicated =
  // false), or on a chosen number of nodes (flexible, with the IC's default
  // counts when none are given).
  enum class HttpOutcallType : uint32_t {
    FullyReplicated = 0,
    NonReplicated = 1,
    Flexible = 2
  };
  struct HttpOutcallReplicationCounts {
    uint32_t total_requests;
    uint32_t min_responses;
    uint32_t max_responses;
  };
  struct HttpOutcallCostParams {
    uint64_t request_bytes{0};
    uint64_t http_roundtrip_time_ms{0};
    uint64_t raw_response_bytes{0};
    uint64_t transformed_response_bytes{0};
    uint64_t transform_instructions{0};
    HttpOutcallType outcall_type{HttpOutcallType::FullyReplicated};
    std::optional<HttpOutcallReplicationCounts> flexible_counts; // Flexible
  };
  static __uint128_t cost_http_request_v2(const HttpOutcallCostParams &params);

  // The cost of a threshold signature or vetKD key derivation, for a key
  // name (e.g. "key_1", "test_key_1") and a curve or algorithm. error tells
  // when the curve/algorithm or the key name is not known; cycles is then 0.
  enum class EcdsaCurve : uint32_t { Secp256k1 = 0 };
  enum class SchnorrAlgorithm : uint32_t { Bip340Secp256k1 = 0, Ed25519 = 1 };
  enum class VetKdCurve : uint32_t { Bls12_381_G2 = 0 };
  enum class SignCostError : uint32_t {
    None = 0,
    InvalidCurveOrAlgorithm = 1,
    InvalidKeyName = 2
  };
  struct SignCost {
    SignCostError error;
    __uint128_t cycles;
  };
  static SignCost cost_sign_with_ecdsa(const std::string &key_name,
                                       EcdsaCurve curve);
  static SignCost cost_sign_with_schnorr(const std::string &key_name,
                                         SchnorrAlgorithm algorithm);
  static SignCost cost_vetkd_derive_key(const std::string &key_name,
                                        VetKdCurve curve); 
}

The IC changes its fees over time, and they depend on the size of the subnet. These methods return what an operation costs right now, on the subnet the canister runs on, so the canister can attach or reserve enough cycles instead of hardcoding a fee.

  • cost_call is what the canister needs above its freezing threshold to make an inter-canister call: the base fee, a fee per byte of method name and argument, and reservations for the largest possible response and its callback. The reservations are partly refunded, so the call can cost less than this, never more.
  • cost_create_canister is the fee of create_canister on the same subnet.
  • cost_http_request is the fee of an HTTPS outcall. Pass the summed byte length of the url, the headers (keys and values), the body and the transform (method name and context), and the max_response_bytes of the request, or 2,000,000 when you don't set it.
  • cost_http_request_v2 is the fee of an HTTPS outcall under pay-as-you-go pricing (pricing_version = 2 of the request), from the resources it uses: the request size, the round-trip time, the raw and transformed response sizes, the instructions of the transform, and how it is replicated: on all nodes of the subnet, on one node (NonReplicated), or on a chosen number of nodes (Flexible).
  • cost_sign_with_ecdsa, cost_sign_with_schnorr and cost_vetkd_derive_key are the fees of a threshold signature or a vetKD key derivation with a key name, such as key_1 or test_key_1. When the curve or algorithm is not known, error is InvalidCurveOrAlgorithm; when the key name is not known for it, error is InvalidKeyName. cycles is then 0.

The methods are available in every entry point, including canister_start. In a native build, the Mock IC returns the fees of a 13-node application subnet, as the local replica reports them, and knows the key names key_1, test_key_1 and dfx_test_key.

Example:

/* file: src/demo_cost.cpp

$ icp canister call demo demo_cost '()' --environment local --query
("Calling greet with 100 bytes: 42102517000 cycles. Creating a canister: 500000000000 cycles. An HTTPS outcall: 158340000 cycles. An Ed25519 signature with test_key_1: 10000000000 cycles.")

*/
#include "demo_cost.h"
#include "ic_api.h"

#include <string>

void demo_cost() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);

  auto cycles = [](__uint128_t v) { return IC_API::to_string_128(v); };

  // A call to "greet" with a 100-byte argument
  std::string msg =
      "Calling greet with 100 bytes: " + cycles(IC_API::cost_call(5, 100)) +
      " cycles. ";
  msg += "Creating a canister: " + cycles(IC_API::cost_create_canister()) +
         " cycles. ";
  // An HTTPS outcall: 1 kB request, at most 10 kB response
  msg +=
      "An HTTPS outcall: " + cycles(IC_API::cost_http_request(1'000, 10'000)) +
      " cycles. ";

  IC_API::SignCost sign = IC_API::cost_sign_with_schnorr(
      "test_key_1", IC_API::SchnorrAlgorithm::Ed25519);
  if (sign.error == IC_API::SignCostError::None) {
    msg += "An Ed25519 signature with test_key_1: " + cycles(sign.cycles) +
           " cycles.";
  } else {
    msg += "test_key_1 cannot sign with Ed25519 here.";
  }
  ic_api.to_wire(CandidTypeText(msg));
}

// file: src/demo_cost.h
#pragma once
#include "wasm_symbol.h"
void demo_cost() WASM_SYMBOL_EXPORTED("canister_query demo_cost");

// file: src/demo.did
service : {
    "demo_cost": () -> (text) query; 
};

inter-canister calls

Declaration:

// ic_async.h
// The reject codes of the IC.
enum class IC_RejectCode : uint32_t {
  NoError = 0,
  SysFatal = 1,
  SysTransient = 2,
  DestinationInvalid = 3,
  CanisterReject = 4,
  CanisterError = 5,
  SysUnknown = 6 // a best-effort call timed out, or its response was lost
};

// Why a call did not produce a reply.
struct IC_CallError {
  enum class Kind : uint32_t {
    // Not performed: the liquid balance can't pay for the call
    InsufficientLiquidCycleBalance,
    // Not performed: ic0.call_perform failed
    CallPerformFailed,
    // Performed, and rejected by the callee or the IC
    CallRejected,
    // Not performed: the cost of a signature or vetKD key is unknown - the
    // key name or the curve/algorithm is not known (IC_Management)
    SignCostFailed,
    // yield_fresh_budget only: the canister is stopping. Reply now with what
    // you have
    CanisterStopping
  };
  Kind kind{Kind::CallRejected};
  IC_RejectCode reject_code{IC_RejectCode::NoError}; // CallRejected
  std::string reject_message;                        // CallRejected
  __uint128_t cycles_refunded{0};                    // CallRejected
  __uint128_t available{0}; // InsufficientLiquidCycleBalance
  __uint128_t required{0};  // InsufficientLiquidCycleBalance
  uint32_t perform_code{0}; // CallPerformFailed
  IC_API::SignCostError sign_cost_error{IC_API::SignCostError::None};

  // The callee did not run the call: retrying can't do it twice.
  bool is_clean_reject() const;
  // A retry may succeed right away.
  bool is_immediately_retryable() const;
  std::string to_string() const;
};

// The reply of a call.
struct IC_CallReply {
  VecBytes bytes;
  __uint128_t cycles_refunded{0};

  // Decode the Candid reply, like ic_api.from_wire(). Traps on a mismatch.
  void decode();
  void decode(CandidType arg);
  void decode(CandidArgs args);
};

// A value or an IC_CallError, shaped like C++23's std::expected.
template <typename T> class [[nodiscard]] IC_CallResult {
public:
  IC_CallResult(T value) : m_v(std::move(value)) {}
  IC_CallResult(IC_CallError error) : m_v(std::move(error)) {}

  bool has_value() const { return m_v.index() == 0; }
  explicit operator bool() const { return has_value(); }

  // Traps when the call failed: handle error() first.
  T &value();
  T *operator->() { return &value(); }
  T &operator*() { return value(); }

  // Traps when the call succeeded.
  const IC_CallError &error() const;

private:
  std::variant<T, IC_CallError> m_v;
};

template <> class [[nodiscard]] IC_CallResult<void> {
public:
  IC_CallResult() = default;
  IC_CallResult(IC_CallError error) : m_e(std::move(error)) {}

  bool has_value() const { return !m_e.has_value(); }
  explicit operator bool() const { return has_value(); }
  void value() const; // Traps when the call failed
  const IC_CallError &error() const;

private:
  std::optional<IC_CallError> m_e;
};

class IC_CallAwaiter;
class IC_CallsAwaiter;

#if defined(ICPP_ALLOW_UNBOUNDED_WAIT)
#define ICPP_UNBOUNDED_WAIT_WARNING
#else
#define ICPP_UNBOUNDED_WAIT_WARNING                                            \
  __attribute__((diagnose_if(                                                  \
      true,                                                                    \
      "IC_Call::unbounded_wait: while this call is in flight, the canister "   \
      "can't be stopped or upgraded, and a callee that never responds blocks " \
      "that forever. Use IC_Call::bounded_wait unless you trust the callee. "  \
      "To turn off this warning, add \"-DICPP_ALLOW_UNBOUNDED_WAIT\" to "      \
      "cpp_compile_flags in both the [build-wasm] and [build-native] "         \
      "sections of icpp.toml.",                                                \
      "warning")))
#endif

// A call to a method of a canister, possibly the canister itself.
class IC_Call {
public:
  // A best-effort call: the IC rejects it with SysUnknown when no response
  // arrived within the timeout (default and maximum 300 s). Use it unless
  // the response must never be lost.
  static IC_Call bounded_wait(const CandidTypePrincipal &callee,
                              const std::string &method);
  // A guaranteed-response call: the reply or reject always arrives, however
  // long the callee takes.
  // WARNING: while it is in flight, the canister can't be stopped, so it can't
  // be upgraded. A callee that never responds blocks that forever. Use it only
  // for a callee you trust. Calling it gives a compiler warning, unless
  // ICPP_ALLOW_UNBOUNDED_WAIT is defined.
  static IC_Call
  unbounded_wait(const CandidTypePrincipal &callee,
                 const std::string &method) ICPP_UNBOUNDED_WAIT_WARNING;

  // The argument. Without one, the call sends no arguments: ().
  IC_Call &with_arg(CandidType arg);
  IC_Call &with_args(CandidArgs args);
  IC_Call &with_raw_args(const std::vector<uint8_t> &bytes);
  // Cycles to send along. The callee accepts what it wants; the rest comes
  // back as cycles_refunded.
  IC_Call &with_cycles(__uint128_t cycles);
  // Bounded-wait calls only. The IC caps the timeout at 300 s. A timeout of
  // 0 is valid, and almost always ends in a SysUnknown reject.
  IC_Call &change_timeout(uint32_t seconds);

  // Performs the call without waiting for, or receiving, a response.
  IC_CallResult<void> oneway();

  IC_CallAwaiter operator co_await() const;

private:
  friend class IC_CallsAwaiter;
  friend class IC_Management;
  // A guaranteed-response call to a trusted callee, without the warning of
  // unbounded_wait: for the management canister helpers
  static IC_Call guaranteed_response(const CandidTypePrincipal &callee,
                                     const std::string &method);
  // Every IC_Call comes from bounded_wait or unbounded_wait
  IC_Call() = default;
  static IC_Call make(const CandidTypePrincipal &callee,
                      const std::string &method,
                      std::optional<uint32_t> timeout_seconds);
  std::vector<uint8_t> m_callee;
  std::string m_method;
  std::vector<uint8_t> m_args;
  __uint128_t m_cycles{0};
  std::optional<uint32_t> m_timeout_seconds;
};

// Several calls at once: they are all performed in this message, and the
// co_await resumes once every response arrived, with the results in the
// order of the calls.
//   std::vector<IC_CallResult<IC_CallReply>> results =
//       co_await IC_CallAll({call_1, call_2, call_3});
class IC_CallAll {
public:
  explicit IC_CallAll(std::vector<IC_Call> calls) : m_calls(std::move(calls)) {}
  class Awaiter;
  Awaiter operator co_await() &&;

private:
  std::vector<IC_Call> m_calls;
};

// The return type of a coroutine that makes calls, started from the exported
// method. It starts right away and runs until its first co_await; the rest
// runs in the callbacks.
class IC_Async {
public:
  struct promise_type;
};

// The return type of a coroutine that another coroutine co_awaits, to get its
// result. It starts when it is awaited - a task that is never awaited never
// runs - and the awaiting coroutine continues when it finishes:
//   IC_Task<std::string> greet_task(CandidTypePrincipal callee) {
//     IC_CallResult<IC_CallReply> r =
//         co_await IC_Call::bounded_wait(callee, "greet");
//     std::string s;
//     r->decode(CandidTypeText{&s});
//     co_return s;
//   }
//   ... std::string s = co_await greet_task(callee);
// Only the coroutine started from the exported method constructs an IC_API;
// a task gets what it needs as arguments. Pass them by value: a task runs
// across messages, after the caller's temporaries are gone.
template <typename T = void> class IC_Task;

// co_await ic_api.yield_fresh_budget() continues in a new message - the
// reply or reject callback of a call to the canister itself - with a fresh
// instruction limit of 38B. The result:
// - success: you are in a new message, with a fresh instruction limit.
// - CanisterStopping: the canister is stopping. Reply now with what you have,
//   and do no further heavy work: the limit may not be fresh.
// - any other error: the call was not performed, no new message started.
//   IC_CallResult<void> r = co_await ic_api.yield_fresh_budget();
//   if (!r) { /* reply with the partial result */ co_return; }
class IC_Yield {
public:
  explicit IC_Yield(IC_Call call);
  class Awaiter;
  Awaiter operator co_await() const;

private:
  IC_Call m_call;
}; 
// ic_api.h
class IC_API {
public:
  // The inter-canister calls this canister has performed whose callback has
  // not run yet. While it is non-zero, canister_pre_upgrade traps: the
  // callbacks would run against the new code. Stop the canister first.
  static uint32_t calls_in_flight();

  // The deadline of a best-effort (bounded wait) call in ns since the epoch,
  // 0 for a guaranteed-response call. Available in update, query, reply &
  // reject callbacks.
  uint64_t get_msg_deadline(); 

  // co_await ic_api.yield_fresh_budget() continues the coroutine in a new
  // message, with a fresh instruction limit, by calling the canister itself.
  // A reply or reject callback gets 38B instructions, where the first message
  // of an update gets 40B. On a stopping canister it returns the error
  // CanisterStopping: reply then with what you have. See ic_async.h. Not
  // available in C++17.
  class IC_Yield yield_fresh_budget(); 
}

A canister calls a method of another canister, or of itself, with co_await on an IC_Call. This uses C++20 coroutines, which icpp-pro builds with by default; the call API is not available in a canister built as C++17.

An update method that makes calls is a coroutine returning IC_Async, started from the exported method, which must stay a plain void function:

IC_Async my_method_async() {
  IC_API ic_api(CanisterUpdate{"my_method"}, false);
  ...
  IC_CallResult<IC_CallReply> result =
      co_await IC_Call::bounded_wait(callee, "greet").with_arg(CandidTypeText{"C++"});
  ...
  ic_api.to_wire(...); // the reply to the caller of my_method
}
void my_method() { my_method_async(); }

An unbounded_wait call can block upgrades

While an unbounded_wait call is in flight, the canister can't be stopped, so it can't be upgraded. If the callee never responds, the canister stays stuck: the only way out under your control is uninstall_code, which wipes all its data, including its files. Use bounded_wait for any callee you don't control: it always ends within its timeout. Keep unbounded_wait for callees you trust, such as the management canister or your own canisters.

Every call of unbounded_wait gives a compiler warning. To turn it off, add "-DICPP_ALLOW_UNBOUNDED_WAIT" to cpp_compile_flags in both the [build-wasm] and [build-native] sections of icpp.toml.

How it works:

  • The code after a co_await runs in a new message: the reply or reject callback of the call, with a limit of 38B instructions. Each co_await is a commit point. If the code after it traps, only what happened after the co_await is rolled back, and the coroutine is freed by the cleanup callback.
  • State set before a commit point stays. A flag such as busy[key] = true, set before the first co_await, stays set forever when a later message traps. An RAII guard does not help: the cleanup destroys the coroutine frame without running its destructors. Give such an entry a time-to-live instead: store when it started, and treat it as expired after a while.
  • The reply goes to the original caller. ic_api.to_wire() after a co_await replies to the call of my_method, and ic_api.get_caller() is still its caller.
  • Arguments. with_arg(CandidType) sends one argument, with_args(CandidArgs) several, and with_raw_args(bytes) sends bytes that are already Candid encoded. Without any of them, the call sends no arguments: ().
  • Every result must be checked. IC_CallResult holds the reply or an IC_CallError. Test it with has_value() or if (result). value(), -> and * trap on an error; error() traps when the call succeeded.
  • Why a call failed. IC_CallError::kind is one of:

    • CallRejected: the callee or the IC rejected the call. reject_code (an IC_RejectCode: SysFatal, SysTransient, DestinationInvalid, CanisterReject, CanisterError or SysUnknown), reject_message and cycles_refunded say more.
    • CallPerformFailed: the call was not performed, because ic0.call_perform returned perform_code.
    • InsufficientLiquidCycleBalance: the call was not performed, because the liquid cycle balance (available) can't pay for it (required, from IC_API::cost_call plus the cycles sent along).
    • CanisterStopping: only from yield_fresh_budget, see below.

    to_string() describes the error. is_clean_reject() is true when the callee did not run the call (SysFatal, SysTransient or DestinationInvalid), so a retry can't run it twice. is_immediately_retryable() is true for SysTransient and SysUnknown, where a retry may succeed right away; after SysUnknown the callee may already have run the call. - Bounded or unbounded wait. A bounded_wait call is a best-effort call: the IC rejects it with SysUnknown when no response arrived within the timeout (default and maximum 300 s). Shorten it with .change_timeout(seconds), where seconds must be > 0. The callee sees the deadline with ic_api.get_msg_deadline(). An unbounded_wait call always gets its response, however long it takes. The ICRC-1 client example retries a transfer after SysUnknown without the risk of transferring twice. - Cycles. with_cycles sends cycles along; what the callee does not accept comes back as cycles_refunded. - No response. oneway() performs the call without co_await, and without waiting for or receiving a response. Its IC_CallResult<void> only says whether the call was performed. It can also be used in a method that is not a coroutine. - A fresh instruction limit. co_await ic_api.yield_fresh_budget() calls the canister itself, and continues in the callback: a new message, with a fresh instruction limit of 38B, and all local variables intact. A computation that does not fit in one message can yield between chunks, and reply once at the end. Its result tells the loop what to do: - success: you are in a new message, with a fresh instruction limit. Any other reject of the self-call, such as SysUnknown or SysTransient, is also a success, because the callback is a new message too. - CanisterStopping: the canister is stopping, for example for an upgrade. Reply now with what you have, and do no further heavy work: the limit may not be fresh, because a canister that is already stopping does not call itself and continues in the same message. Without this, every stop or upgrade would wait for the loop to end. - any other error: the call was not performed, and no new message started.

    C++ IC_CallResult<void> r = co_await ic_api.yield_fresh_budget(); if (!r) { ic_api.to_wire(...); // the partial result co_return; } - Upgrades. The callbacks of calls in flight would run against the new code, with the heap of the old version gone. A canister that makes calls therefore refuses to upgrade while calls are in flight: canister_pre_upgrade traps. icp canister install --mode upgrade stops the canister first, which waits until all calls returned, so it upgrades safely. Stop the canister yourself (icp canister stop) when you upgrade another way. A canister that defines its own canister_pre_upgrade gets the same check from its IC_API(CanisterPreUpgrade{...}).

What a call can do:

  • Several calls, one after the other. A coroutine can co_await as many calls as it needs, in loops and branches, each one waiting for the previous to finish.
  • A chain of calls. The method a call runs can be a coroutine too, and make its own calls before it replies: A calls B, B calls C, C replies to B, and B replies to A.
  • A call back into the caller. A callee can call the canister that is waiting for it (A calls B, B calls A), or a canister can call itself. A runs the new call as a separate message, while its first call is still waiting.

A coroutine that awaits another coroutine. A coroutine returning IC_Task<T> can be awaited, and gives its result:

IC_Task<IC_CallResult<std::string>> greet_task(CandidTypePrincipal callee, std::string name) {
  IC_CallResult<IC_CallReply> r =
      co_await IC_Call::bounded_wait(callee, "greet").with_arg(CandidTypeText{name});
  if (!r) co_return r.error();
  std::string greeting;
  r->decode(CandidTypeText{&greeting});
  co_return greeting;
}

IC_Async my_method_async() {
  IC_API ic_api(CanisterUpdate{"my_method"}, false);
  ...
  IC_CallResult<std::string> greeting = co_await greet_task(callee, "C++");
  ...
}
  • A task is not a call. An IC_Call is a message to a canister: co_await on it sends the message, and the code after it runs in a new message, the callback of the call. An IC_Task is code of your own canister: co_await on it sends nothing, and runs the task in the same message, as a function call would, until the task awaits a call. The whole chain then waits for that response. When the task does co_return, the coroutine awaiting it continues right away, in the same message.
  • Three roles. The IC_Async coroutine is the root: the exported method starts it, and it owns the IC_API. An IC_Task<T> is a step of it that you await for a result. An IC_Call is the only one that reaches another canister.
  • A task starts when it is awaited, and the awaiting coroutine continues when the task finishes. A task that is never awaited never runs. A task can be awaited once: a second co_await on it traps. A task can await other tasks, any number of levels deep.
  • Only the coroutine started from the exported method constructs the IC_API. A task gets what it needs as arguments: pass them by value, because the task runs across messages.
  • If the code after a co_await traps, in a task or in the coroutine awaiting it, the cleanup frees every coroutine of the chain.

Several calls in flight at once. IC_CallAll performs its calls in the same message, and the co_await continues once every response arrived, with the results in the order of the calls:

std::vector<IC_CallResult<IC_CallReply>> results = co_await IC_CallAll({
    IC_Call::bounded_wait(ledger, "icrc1_balance_of").with_arg(account),
    IC_Call::bounded_wait(other, "get_price"),
});
  • Every result is checked on its own: one call can be rejected while the others succeed. A call that can't be performed gets its error right away, and the others still go ahead.
  • Each response arrives as its own message, and the coroutine continues in the message of the last one.
  • A coroutine has one co_await in flight at a time: to wait for several calls, put them in one IC_CallAll.

Calls are available in update methods, composite queries, their callbacks, heartbeat and timers; not in a plain query (see composite queries). In a native build, the Mock IC delivers the calls after the method returns: a call to the canister itself runs the method registered with mockIC.register_method(name, fn), and the response of another canister is set with mockIC.expect_call(callee, method, MockIC::MockResponse::reply(hex)), reject(code, message) or timeout(). After mockIC.set_canister_status(MOCKIC_CANISTER_STATUS_STOPPING), the Mock IC rejects every call to the canister itself, like the IC, and still delivers the responses of calls in flight. A trap after a co_await counts as a trap, like one before it, also when another coroutine of the same call replies later: it fails mockIC.run_test, and passes mockIC.run_trap_test.

To test calls that interleave, turn off the automatic delivery with mockIC.set_auto_drain(false), start each call with MockIC::CallHandle h = mockIC.start_call(name, fn, candid_in, caller), and deliver the queued calls and responses with mockIC.drain(), or one at a time with mockIC.drain_one(). Then mockIC.call_done(h), was_rejected(h), reply_hex(h) and reject_message(h) tell how each call ended. Between two drain_one() steps, a test can change the mock state, for example the canister status.

Example:

/* file: src/demo_calls.cpp

$ icp canister call demo demo_call '("C++")' --environment local
("The callee replied: Hello, C++!")

$ icp canister call demo demo_yield '(3 : nat32)' --environment local
(
  "Instructions per message: 1207587 1074949 1074599. The whole call: 3613406 instructions.",
)

$ icp canister call demo demo_long_compute '(150 : nat32)' --environment local
(
  "150B instructions in 8 messages, at most 20B each. One message can execute at most 40B. Result: 11403713523426525226",
)

*/
#include "demo_calls.h"
#include "ic_api.h"

#include <string>

// Calls demo_call_callee of this canister, and replies with its reply.
// demo_call is a coroutine: it starts in the update call, and continues in
// the callback of the call it awaits.
IC_Async demo_call_async() {
  IC_API ic_api(CanisterUpdate{"demo_call"}, false);
  std::string name;
  ic_api.from_wire(CandidTypeText{&name});

  // Any canister can be called; here, the canister itself
  IC_CallResult<IC_CallReply> result =
      co_await IC_Call::bounded_wait(ic_api.get_canister_self(),
                                     "demo_call_callee")
          .with_arg(CandidTypeText{name});
  if (!result) {
    ic_api.to_wire(
        CandidTypeText{"The call failed: " + result.error().to_string()});
    co_return;
  }
  std::string greeting;
  result->decode(CandidTypeText{&greeting});
  ic_api.to_wire(CandidTypeText{"The callee replied: " + greeting});
}
void demo_call() { demo_call_async(); }

void demo_call_callee() {
  IC_API ic_api(CanisterUpdate{std::string(__func__)}, false);
  std::string name;
  ic_api.from_wire(CandidTypeText{&name});
  ic_api.to_wire(CandidTypeText{"Hello, " + name + "!"});
}

// Continues in a new message after every yield, so a long computation
// is not stopped by the instruction limit of one message.
IC_Async demo_yield_async() {
  IC_API ic_api(CanisterUpdate{"demo_yield"}, false);
  uint32_t n{0};
  ic_api.from_wire(CandidTypeNat32{&n});
  if (n > 10) {
    IC_API::trap("demo_yield: at most 10 yields");
  }

  std::string msg = "Instructions per message:";
  for (uint32_t i = 0; i < n; ++i) {
    // A chunk of work...
    volatile uint64_t sum = 0;
    for (uint64_t k = 0; k < 100'000; ++k)
      sum += k;
    msg += " " + std::to_string(IC_API::performance_counter());
    // ...then continue in a new message, with a fresh instruction limit
    IC_CallResult<void> yielded = co_await ic_api.yield_fresh_budget();
    yielded.value();
  }
  msg += ". The whole call: " +
         std::to_string(IC_API::performance_counter(
             IC_API::PerformanceCounterType::CallContextInstructions)) +
         " instructions.";
  ic_api.to_wire(CandidTypeText{msg});
}
void demo_yield() { demo_yield_async(); }

// A computation of `billions` billion instructions: up to 200B, five times
// the 40B limit of one message. It runs in chunks of at most 20B
// instructions, with a yield after each, and replies once it is done.
// It burns cycles, so only a controller may call it.
IC_Async demo_long_compute_async() {
  IC_API ic_api(CanisterUpdate{"demo_long_compute"}, false);
  uint32_t billions{0};
  ic_api.from_wire(CandidTypeNat32{&billions});
  if (!ic_api.is_controller(ic_api.get_caller())) {
    IC_API::trap("demo_long_compute: only a controller may call it");
  }
  if (billions > 200) {
    IC_API::trap("demo_long_compute: at most 200 billion instructions");
  }
  const uint64_t target = uint64_t{billions} * 1'000'000'000;
  const uint64_t chunk = 20'000'000'000;

  auto total = [] {
    return IC_API::performance_counter(
        IC_API::PerformanceCounterType::CallContextInstructions);
  };

  // The state of the computation: an LCG, x = a * x + c (mod 2^64).
  // It lives in the coroutine, so it survives the yields.
  uint64_t x = 42;
  uint32_t messages = 0;
  while (true) {
    ++messages;
    // A chunk of work, until this message executed `chunk` instructions,
    // or the whole call `target` instructions...
    do {
      for (int i = 0; i < 65536; ++i)
        x = 6364136223846793005ULL * x + 1442695040888963407ULL;
    } while (IC_API::performance_counter() < chunk && total() < target);
    if (total() >= target) break;
    // ...then continue in a new message, with a fresh instruction limit
    IC_CallResult<void> yielded = co_await ic_api.yield_fresh_budget();
    yielded.value();
  }
  ic_api.to_wire(CandidTypeText{
      std::to_string(total() / 1'000'000'000) + "B instructions in " +
      std::to_string(messages) +
      " messages, at most 20B each. One message can execute at most 40B. "
      "Result: " +
      std::to_string(x)});
}
void demo_long_compute() { demo_long_compute_async(); }

// file: src/demo_calls.h
#pragma once
#include "wasm_symbol.h"
void demo_call() WASM_SYMBOL_EXPORTED("canister_update demo_call");
void demo_call_callee()
    WASM_SYMBOL_EXPORTED("canister_update demo_call_callee");
void demo_yield() WASM_SYMBOL_EXPORTED("canister_update demo_yield");
void demo_long_compute()
    WASM_SYMBOL_EXPORTED("canister_update demo_long_compute");

// file: src/demo.did
service : {
    "demo_call": (text) -> (text);
    "demo_call_callee": (text) -> (text);
    // demo_yield: at most 10 yields
    "demo_yield": (nat32) -> (text);
    // demo_long_compute: controllers only, at most 200 (billion instructions)
    "demo_long_compute": (nat32) -> (text); 
};

composite queries

A composite query is a query that calls other queries: query and composite query methods of canisters on the same subnet, including the canister itself. It is free and fast like any query, and nothing it does is kept.

// my_canister.h
void my_query() WASM_SYMBOL_EXPORTED("canister_composite_query my_query");
// my_canister.did
"my_query" : (principal, text) -> (text) composite_query;
IC_Async my_query_async() {
  IC_API ic_api(CanisterCompositeQuery{"my_query"}, false);
  ...
  IC_CallResult<IC_CallReply> r =
      co_await IC_Call::bounded_wait(callee, "greet_q").with_arg(CandidTypeText{name});
  ...
  ic_api.to_wire(...);
}
void my_query() { my_query_async(); }

It works like an update method that makes calls (IC_Call, IC_CallAll, IC_Task, the reply after the co_await), with the rules of a query:

  • Only query and composite query methods can be called, only on the same subnet. A call to an update method is rejected, and an update method can't call a composite query.
  • No cycles: a call can't send cycles along (with_cycles traps), and the IC never counts the calls in flight: they can't block an upgrade.
  • No timers or certified data, in the composite query nor in the callbacks of its calls. Only the top-level query sees the data certificate (data_certificate_present() is false in a callback).
  • Limits of the IC: a call graph at most 6 calls deep, 5 billion instructions for the whole call graph (plus 50 million per call), and 10 seconds.
  • State changes are discarded when the query ends, also those made before a co_await. The Mock IC keeps them, so a native test should not rely on the discard.

In a native build, register a query or composite query method of the canister with its kind, so the Mock IC applies the same rules: mockIC.register_method("greet_q", greet_q, MockIC::MethodKind::Query).


management canister

Declaration:

// ic_management.h
// The settings of a new canister; a field left empty gets the IC's default
struct IC_CanisterSettings {
  std::optional<std::vector<std::string>> controllers; // principal texts
  std::optional<__uint128_t> compute_allocation;
  std::optional<__uint128_t> memory_allocation;
  std::optional<__uint128_t> freezing_threshold;
  std::optional<__uint128_t> reserved_cycles_limit;
  std::optional<__uint128_t> wasm_memory_limit;
};

// The threshold keys, e.g. "key_1" on mainnet, "test_key_1" (mainnet and a
// local network) and "dfx_test_key" (a local network)
struct IC_EcdsaKeyId {
  IC_API::EcdsaCurve curve{IC_API::EcdsaCurve::Secp256k1};
  std::string name;
};
struct IC_SchnorrKeyId {
  IC_API::SchnorrAlgorithm algorithm{IC_API::SchnorrAlgorithm::Ed25519};
  std::string name;
};
struct IC_VetKdKeyId {
  IC_API::VetKdCurve curve{IC_API::VetKdCurve::Bls12_381_G2};
  std::string name;
};

struct IC_PublicKey {
  std::vector<uint8_t> public_key;
  std::vector<uint8_t> chain_code;
};

using IC_Blob = std::vector<uint8_t>;

// Calls to the management canister (aaaaa-aa), the IC's system API. Each
// helper encodes the Candid argument, attaches the cycles the call costs, and
// decodes the reply.
class IC_Management {
public:
  // 32 random bytes from the IC's random beacon, which no node can predict.
  // Execution is deterministic, so this is a canister's only source of secure
  // randomness.
  static IC_Task<IC_CallResult<IC_Blob>> raw_rand();

  // Creates an empty canister, e.g. one per user, controlled by this
  // canister unless settings say otherwise. It starts with extra_cycles, attached on top of the cost of
  // the creation: a canister needs cycles to allocate memory, so give it some
  // (the IC rejects the creation with an "insufficient cycles" error
  // otherwise).
  static IC_Task<IC_CallResult<CandidTypePrincipal>>
  create_canister(IC_CanisterSettings settings = {},
                  __uint128_t extra_cycles = 0);

  // Threshold ECDSA (secp256k1): the private key is spread over the nodes of
  // a signing subnet and never exists in one place. The public key gives a
  // Bitcoin or Ethereum address that this canister controls; sign_with_ecdsa
  // signs a 32-byte hash, e.g. of a transaction.
  static IC_Task<IC_CallResult<IC_PublicKey>>
  ecdsa_public_key(IC_EcdsaKeyId key, std::vector<IC_Blob> derivation_path,
                   std::optional<CandidTypePrincipal> canister_id = {});
  static IC_Task<IC_CallResult<IC_Blob>>
  sign_with_ecdsa(IC_Blob message_hash, std::vector<IC_Blob> derivation_path,
                  IC_EcdsaKeyId key);

  // Threshold Schnorr: bip340secp256k1 for Bitcoin Taproot (with an optional
  // bip341_merkle_root_hash), ed25519 for e.g. Solana. Like ECDSA, no private
  // key exists in one place.
  static IC_Task<IC_CallResult<IC_PublicKey>>
  schnorr_public_key(IC_SchnorrKeyId key, std::vector<IC_Blob> derivation_path,
                     std::optional<CandidTypePrincipal> canister_id = {});
  static IC_Task<IC_CallResult<IC_Blob>>
  sign_with_schnorr(IC_Blob message, std::vector<IC_Blob> derivation_path,
                    IC_SchnorrKeyId key,
                    std::optional<IC_Blob> bip341_merkle_root_hash = {});

  // vetKD (vetKeys): derives a secret key for an input, encrypted to the
  // client's transport public key, so no node sees it. For end-to-end
  // encryption, with this canister deciding who gets which key.
  static IC_Task<IC_CallResult<IC_Blob>>
  vetkd_public_key(IC_VetKdKeyId key, IC_Blob context,
                   std::optional<CandidTypePrincipal> canister_id = {});
  static IC_Task<IC_CallResult<IC_Blob>>
  vetkd_derive_key(IC_Blob input, IC_Blob context, IC_Blob transport_public_key,
                   IC_VetKdKeyId key);

  // An HTTPS request to a server on the internet, e.g. a price feed
  static IC_Task<IC_CallResult<IC_HttpOutcallResponse>>
  http_request(IC_HttpOutcallRequest request);
}; 

The management canister (aaaaa-aa) is the IC's system API: it does what a canister can't do with its own code, such as creating canisters, giving secure randomness, signing with threshold keys, and calling the internet. You could call it with a plain IC_Call, but its arguments are nested Candid, and most of its methods must be paid for. IC_Management has a helper per method, named as in the IC's interface: it encodes the argument, attaches exactly the cycles the call costs, picks the right kind of wait, and decodes the reply.

Helper What it does What it is for Cost & wait
raw_rand 32 random bytes from the IC's random beacon, which no node can predict or bias Lotteries, random ids, nonces, seeding a PRNG. Execution is deterministic, so this is the only source of secure randomness free, bounded wait
create_canister Creates an empty canister, controlled by this canister unless settings say otherwise. It starts with extra_cycles A factory, e.g. a canister per user, or scaling out storage cost_create_canister + extra_cycles, guaranteed response
ecdsa_public_key / sign_with_ecdsa The public key, and a signature of a 32-byte hash, with a threshold secp256k1 key. The private key is spread over the nodes of a signing subnet, and never exists in one place A Bitcoin or Ethereum address that the canister controls, and signing its transactions public key: free, bounded wait
signature: cost_sign_with_ecdsa, guaranteed response
schnorr_public_key / sign_with_schnorr The same with a threshold Schnorr key: bip340secp256k1 (optionally with a BIP341 tweak) or ed25519 Bitcoin Taproot, and ed25519 chains such as Solana public key: free, bounded wait
signature: cost_sign_with_schnorr, guaranteed response
vetkd_public_key / vetkd_derive_key vetKeys: derives a secret key for an input, encrypted to the client's transport public key, so no node sees it End-to-end encryption, with the canister deciding who gets which key: encrypted notes or messages, a key per user public key: free, bounded wait
key: cost_vetkd_derive_key, guaranteed response
http_request An HTTPS request to a server on the internet (see HTTPS outcalls) Price feeds, external APIs, webhooks cost_http_request, guaranteed response

Each helper is an IC_Task, awaited from a coroutine:

IC_Async my_method_async() {
  IC_API ic_api(CanisterUpdate{"my_method"}, false);
  ...
  IC_CallResult<IC_Blob> random = co_await IC_Management::raw_rand();

  IC_EcdsaKeyId key{IC_API::EcdsaCurve::Secp256k1, "test_key_1"};
  IC_CallResult<IC_Blob> signature =
      co_await IC_Management::sign_with_ecdsa(message_hash, {}, key);
  ...
}
  • The cycles are attached for you, from the cycle cost functions: cost_create_canister, cost_sign_with_ecdsa, cost_sign_with_schnorr, cost_vetkd_derive_key and cost_http_request.
  • A call that attaches cycles makes a guaranteed-response call, like in the Rust CDK: with a bounded-wait call, the cycles could be lost on a timeout. It does not give the unbounded_wait warning. The other helpers make bounded-wait calls.
  • An upgrade waits for these calls to return, usually seconds. Like any call in flight, they keep the canister from stopping. An unknown callee can hold a call open for as long as it likes, and so block an upgrade forever. The management canister is part of the IC: it works through each request, and replies or rejects. An HTTPS outcall gets its response, or a timeout error, within 60 seconds. A signing or vetKD request is rejected right away when the queue for its key is full, and a subnet can set an expiry, after which a request still waiting is rejected (SysTransient, "Chain key request expired").
  • The threshold keys are named key_1 (mainnet), test_key_1 (mainnet and a local network) and dfx_test_key (a local network). For a key name that is not known, the helper does not make the call: the error is IC_CallError::Kind::SignCostFailed.
  • A new canister starts with extra_cycles, attached on top of the cost of its creation. It needs some to allocate memory: with none, the IC rejects the creation.
  • In a native build, the Mock IC answers the management canister itself, with deterministic bytes of the right length for raw_rand, create_canister, the public keys, the signatures and the vetKD keys. Set the response of an HTTPS outcall with mockIC.expect_call("aaaaa-aa", "http_request", ...).

Example:

/* file: src/demo_management.cpp

$ icp canister call demo demo_raw_rand '()' --environment local
(
  "32 random bytes: dac93bd0b5ca3d5e6b0a54beb06961c9edf8b5038a02c57e5d1f4d45de4cd97f",
)

$ icp canister call demo demo_sign '("icpp")' --environment local
(
  "public key 026b3965713aecb522045d7fae01bb6aca6ff2dc3954c2ed13e7848a25db3d40f2, signature e8bc1ec8741f59a8af6081c1c692903bd38f1890fbbbc0bf8a0b1b972bbba9112c68d871b6d023df7dd4f4c6c2817d6db684e11af1db50376d2d4c0dc8b0bc51",
)

*/
#include "demo_management.h"
#include "ic_api.h"

#include <string>

namespace {
std::string hex(const IC_Blob &bytes) {
  const char *digits = "0123456789abcdef";
  std::string s;
  for (uint8_t b : bytes) {
    s += digits[b >> 4];
    s += digits[b & 0xf];
  }
  return s;
}
} // namespace

// 32 random bytes from the management canister
IC_Async demo_raw_rand_async() {
  IC_API ic_api(CanisterUpdate{"demo_raw_rand"}, false);
  ic_api.from_wire();
  IC_CallResult<IC_Blob> random = co_await IC_Management::raw_rand();
  ic_api.to_wire(CandidTypeText{random ? "32 random bytes: " + hex(*random)
                                       : "raw_rand failed: " +
                                             random.error().to_string()});
}
void demo_raw_rand() { demo_raw_rand_async(); }

// Signs a 32-byte digest of a message with threshold ECDSA. The cycles of the
// signature are attached for you. test_key_1 exists on mainnet and on a
// local network.
IC_Async demo_sign_async() {
  IC_API ic_api(CanisterUpdate{"demo_sign"}, false);
  std::string message;
  ic_api.from_wire(CandidTypeText{&message});

  // An illustrative XOR fold, not a cryptographic hash: a real application
  // signs the SHA-256 of the message.
  IC_Blob message_hash(32, 0);
  for (size_t i = 0; i < message.size(); ++i)
    message_hash[i % 32] ^= (uint8_t)message[i];

  IC_EcdsaKeyId key{IC_API::EcdsaCurve::Secp256k1, "test_key_1"};
  std::vector<IC_Blob> derivation_path{{'d', 'e', 'm', 'o'}};
  IC_CallResult<IC_PublicKey> public_key =
      co_await IC_Management::ecdsa_public_key(key, derivation_path);
  IC_CallResult<IC_Blob> signature = co_await IC_Management::sign_with_ecdsa(
      message_hash, derivation_path, key);
  if (!public_key || !signature) {
    ic_api.to_wire(CandidTypeText{
        "signing failed: " + (public_key ? signature.error().to_string()
                                         : public_key.error().to_string())});
    co_return;
  }
  ic_api.to_wire(CandidTypeText{"public key " + hex(public_key->public_key) +
                                ", signature " + hex(*signature)});
}
void demo_sign() { demo_sign_async(); }

// file: src/demo_management.h
#pragma once
#include "wasm_symbol.h"
void demo_raw_rand() WASM_SYMBOL_EXPORTED("canister_update demo_raw_rand");
void demo_sign() WASM_SYMBOL_EXPORTED("canister_update demo_sign");

// file: src/demo.did
service : {
    "demo_raw_rand": () -> (text);
    "demo_sign": (text) -> (text); 
};

HTTPS outcalls

Declaration:

// ic_http.h
enum class IC_HttpMethod { Get, Head, Post, Put, Delete, Patch };

struct IC_HttpOutcallRequest {
  std::string url;
  IC_HttpMethod method{IC_HttpMethod::Get};
  std::vector<IC_HeaderField> headers;
  std::optional<std::vector<uint8_t>> body;
  // The largest response the canister accepts, and pays for (default and
  // maximum: 2 MB)
  std::optional<uint64_t> max_response_bytes;
  // A query of this canister that the response passes through on every node,
  // e.g. to remove headers that differ per node, so the nodes agree on it
  std::optional<std::string> transform_method;
  std::vector<uint8_t> transform_context;
  // false: the request is sent by one node instead of every node of the
  // subnet. Cheaper, but the response is not agreed on by consensus.
  std::optional<bool> is_replicated;
};

struct IC_HttpOutcallResponse {
  __uint128_t status{0};
  std::vector<IC_HeaderField> headers;
  std::vector<uint8_t> body;
};

// The argument of a transform query
struct IC_HttpTransformArgs {
  IC_HttpOutcallResponse response;
  std::vector<uint8_t> context;
}; 
// ic_api.h
class IC_API {
public:
  void from_wire(IC_HttpTransformArgs &args);
  void
  to_wire(const IC_HttpOutcallResponse &response); 
}

co_await IC_Management::http_request(request) sends an HTTP request to a server on the internet, and gives its response.

  • Replicated, with a transform. By default every node of the subnet sends the request, and the nodes must agree on the response. Responses usually differ per node (a date header, ...), so pass the name of a query of the canister as transform_method: every node passes its response through it, and the nodes agree on what it returns.
  • Non-replicated. With is_replicated = false, one node sends the request. It is cheaper and needs no transform, but the response is not agreed on by consensus: use it only when you can trust the server's response, or verify it.
  • A transform query reads its argument with ic_api.from_wire(IC_HttpTransformArgs &) and replies with ic_api.to_wire(const IC_HttpOutcallResponse &):
void transform() WASM_SYMBOL_EXPORTED("canister_query transform");
void transform() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  IC_HttpTransformArgs args;
  ic_api.from_wire(args);
  args.response.headers.clear(); // the headers differ per node
  ic_api.to_wire(args.response);
}

ICRC-1 client

An example of calls to another canister: the icrc1_client demo calls an ICRC-1 ledger. Its icp.yaml deploys a pinned ic-icrc1-ledger wasm next to it.

  • token_info asks the ledger for its symbol, fee and decimals with one IC_CallAll: the three calls are in flight at the same time.
  • transfer shows how to handle a SysUnknown error: the transfer may or may not have happened. The transfer is sent with a created_at_time, and the retry uses the same one. The ledger then recognizes the retry as the same transfer: if the first one went through, it answers Duplicate instead of transferring twice.
  • In a native build, the ledger's responses are set with mockIC.expect_call, including a timeout() followed by a Duplicate, to test the retry.

Example:

/* file: src/icrc1_client.cpp

An ICRC-1 client: it calls the ICRC-1 methods of a ledger canister.

$ icp canister call icrc1_client token_info '(principal "<ledger>")' --environment local
("TEST: fee 10000, 8 decimals")

$ icp canister call icrc1_client balance '(principal "<ledger>")' --environment local
(998_990_000 : nat)

$ icp canister call icrc1_client transfer '(principal "<ledger>", principal "<to>", 1_000_000 : nat)' --environment local
("Ok: block 2")

*/
#include "icrc1_client.h"
#include "ic_api.h"

#include <string>

namespace {
// An ICRC-1 account: record { owner : principal; subaccount : opt blob }.
// Without a subaccount, the field is left out (it is an opt).
CandidTypeRecord account(const std::string &owner) {
  CandidTypeRecord record;
  record.append("owner", CandidTypePrincipal{owner});
  return record;
}

// The first argument of every method: the ledger
CandidTypePrincipal ledger_arg(IC_API &ic_api, CandidArgs &args,
                               std::string &ledger) {
  ic_api.from_wire(args);
  return CandidTypePrincipal{ledger};
}
} // namespace

// Three queries of the ledger, made at the same time with IC_CallAll
IC_Async token_info_async() {
  IC_API ic_api(CanisterUpdate{"token_info"}, false);
  std::string ledger_text;
  CandidArgs args;
  args.append(CandidTypePrincipal{&ledger_text});
  CandidTypePrincipal ledger = ledger_arg(ic_api, args, ledger_text);

  std::vector<IC_CallResult<IC_CallReply>> results = co_await IC_CallAll({
      IC_Call::bounded_wait(ledger, "icrc1_symbol"),
      IC_Call::bounded_wait(ledger, "icrc1_fee"),
      IC_Call::bounded_wait(ledger, "icrc1_decimals"),
  });
  for (IC_CallResult<IC_CallReply> &r : results) {
    if (!r) {
      ic_api.to_wire(CandidTypeText{"failed: " + r.error().to_string()});
      co_return;
    }
  }
  std::string symbol;
  __uint128_t fee{0};
  uint8_t decimals{0};
  results[0]->decode(CandidTypeText{&symbol});
  results[1]->decode(CandidTypeNat{&fee});
  results[2]->decode(CandidTypeNat8{&decimals});
  ic_api.to_wire(CandidTypeText{symbol + ": fee " + IC_API::to_string_128(fee) +
                                ", " + std::to_string(decimals) + " decimals"});
}
void token_info() { token_info_async(); }

// icrc1_balance_of the account of this canister
IC_Async balance_async() {
  IC_API ic_api(CanisterUpdate{"balance"}, false);
  std::string ledger_text;
  CandidArgs args;
  args.append(CandidTypePrincipal{&ledger_text});
  CandidTypePrincipal ledger = ledger_arg(ic_api, args, ledger_text);

  IC_CallResult<IC_CallReply> r =
      co_await IC_Call::bounded_wait(ledger, "icrc1_balance_of")
          .with_arg(account(ic_api.get_canister_self().get_text()));
  __uint128_t balance{0};
  r->decode(CandidTypeNat{&balance});
  ic_api.to_wire(CandidTypeNat{balance});
}
void balance() { balance_async(); }

// icrc1_transfer from this canister's account. The call is a bounded-wait
// call: when it times out (SysUnknown), the transfer may or may not have
// happened. It is retried with the same created_at_time, which makes the
// ledger see the retry as the same transfer: if the first one went through,
// the ledger answers Duplicate instead of transferring twice.
IC_Async transfer_async() {
  IC_API ic_api(CanisterUpdate{"transfer"}, false);
  std::string ledger_text, to;
  __uint128_t amount{0};
  CandidArgs args;
  args.append(CandidTypePrincipal{&ledger_text});
  args.append(CandidTypePrincipal{&to});
  args.append(CandidTypeNat{&amount});
  CandidTypePrincipal ledger = ledger_arg(ic_api, args, ledger_text);

  // record { to : Account; amount : nat; created_at_time : opt nat64 }
  // The other fields - from_subaccount, fee, memo - are opts, left out.
  CandidTypeRecord transfer_arg;
  transfer_arg.append("to", account(to));
  transfer_arg.append("amount", CandidTypeNat{amount});
  transfer_arg.append("created_at_time", CandidTypeOptNat64{IC_API::time()});

  IC_CallResult<IC_CallReply> r =
      co_await IC_Call::bounded_wait(ledger, "icrc1_transfer")
          .with_arg(transfer_arg);
  if (!r && r.error().reject_code == IC_RejectCode::SysUnknown) {
    r = co_await IC_Call::bounded_wait(ledger, "icrc1_transfer")
            .with_arg(transfer_arg);
  }
  if (!r) {
    ic_api.to_wire(CandidTypeText{"failed: " + r.error().to_string()});
    co_return;
  }

  // variant { Ok : nat; Err : TransferError }
  std::string label, error_label, message;
  __uint128_t block{0}, expected_fee{0}, min_burn_amount{0}, balance{0},
      duplicate_of{0}, error_code{0};
  uint64_t ledger_time{0};
  CandidTypeRecord bad_fee, bad_burn, insufficient_funds, created_in_future,
      duplicate, generic_error;
  bad_fee.append("expected_fee", CandidTypeNat{&expected_fee});
  bad_burn.append("min_burn_amount", CandidTypeNat{&min_burn_amount});
  insufficient_funds.append("balance", CandidTypeNat{&balance});
  created_in_future.append("ledger_time", CandidTypeNat64{&ledger_time});
  duplicate.append("duplicate_of", CandidTypeNat{&duplicate_of});
  generic_error.append("error_code", CandidTypeNat{&error_code});
  generic_error.append("message", CandidTypeText{&message});
  CandidTypeVariant transfer_error{&error_label};
  transfer_error.append("BadFee", bad_fee);
  transfer_error.append("BadBurn", bad_burn);
  transfer_error.append("InsufficientFunds", insufficient_funds);
  transfer_error.append("TooOld", CandidTypeNull{});
  transfer_error.append("CreatedInFuture", created_in_future);
  transfer_error.append("TemporarilyUnavailable", CandidTypeNull{});
  transfer_error.append("Duplicate", duplicate);
  transfer_error.append("GenericError", generic_error);
  CandidTypeVariant result{&label};
  result.append("Ok", CandidTypeNat{&block});
  result.append("Err", transfer_error);
  r->decode(result);

  std::string text;
  if (label == "Ok") {
    text = "Ok: block " + IC_API::to_string_128(block);
  } else if (error_label == "InsufficientFunds") {
    text = "Err: InsufficientFunds, balance " + IC_API::to_string_128(balance);
  } else if (error_label == "BadFee") {
    text = "Err: BadFee, expected " + IC_API::to_string_128(expected_fee);
  } else if (error_label == "Duplicate") {
    text = "Err: Duplicate of block " + IC_API::to_string_128(duplicate_of);
  } else if (error_label == "GenericError") {
    text = "Err: GenericError " + IC_API::to_string_128(error_code) + ", " +
           message;
  } else {
    text = "Err: " + error_label;
  }
  ic_api.to_wire(CandidTypeText{text});
}
void transfer() { transfer_async(); }

// file: src/icrc1_client.did
service : {
  // The token of an ICRC-1 ledger: '(ledger)' -> '("TEST: fee 10000, 8 decimals")'
  "token_info": (principal) -> (text);
  // The balance of this canister: '(ledger)' -> '(nat)'
  "balance": (principal) -> (nat);
  // A transfer from this canister: '(ledger, to, amount)' -> '("Ok: block 1")'
  "transfer": (principal, principal, nat) -> (text); 
};

to_wire

Declaration:

// ic_api.h
class IC_API {
public:
  void to_wire();
  void to_wire(const CandidType &arg_out);
  void to_wire(const CandidArgs &args_out);
  void to_wire(const IC_HttpResponse response); 
}

Sends data to the wire in Candid format, assuming either no arguments, a single argument of a CandidType object, or aCandidArgs object to send multiple arguments.

C++ data structures are passed into the CandidType objects, and the to_wirefunction will serialize these values into a Candid byte stream to be sent over the wire.

Example:

Here is how to use to_wire() in your C++ method:

/* file: src/demo_to_wire.cpp

$ icp canister call demo demo_to_wire_no_arg '()' --environment local


$ icp canister call demo demo_to_wire_one_arg '("Neuron Staking")' --environment local
("Hello expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae, your hobby is Neuron Staking")

$ icp canister call demo demo_to_wire_multiple_args '("Neuron Staking", 3000 : nat64)' --environment local
("expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae", "Neuron Staking", 3_000 : nat64, "ICP")

*/
#include "demo_to_wire.h"
#include "ic_api.h"

void demo_to_wire_no_arg() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  ic_api.from_wire();
  ic_api.to_wire();
}

void demo_to_wire_one_arg() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string hobby{""};
  ic_api.from_wire(CandidTypeText{&hobby});

  std::string msg;
  msg.append("Hello " + caller.get_text() + ", ");
  msg.append("your hobby is " + hobby);

  ic_api.to_wire(CandidTypeText(msg));
}

void demo_to_wire_multiple_args() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  std::string hobby{""};
  uint64_t num_icp{0};
  CandidArgs args_in;
  args_in.append(CandidTypeText(&hobby));
  args_in.append(CandidTypeNat64(&num_icp));
  ic_api.from_wire(args_in);

  CandidArgs args_out;
  args_out.append(CandidTypeText(caller.get_text()));
  args_out.append(CandidTypeText(hobby));
  args_out.append(CandidTypeNat64(num_icp));
  args_out.append(CandidTypeText("ICP"));
  ic_api.to_wire(args_out);
}
// file: src/demo_to_wire.h
#pragma once
#include "wasm_symbol.h"
void demo_to_wire_no_arg()
    WASM_SYMBOL_EXPORTED("canister_query demo_to_wire_no_arg");
void demo_to_wire_one_arg()
    WASM_SYMBOL_EXPORTED("canister_query demo_to_wire_one_arg");
void demo_to_wire_multiple_args()
    WASM_SYMBOL_EXPORTED("canister_query demo_to_wire_multiple_args");
// file: src/demo.did
service : {
    "demo_to_wire_no_arg": () -> () query;
    "demo_to_wire_one_arg": (text) -> (text) query;
    "demo_to_wire_multiple_args": (text, nat64) -> (text, text, nat64, text) query; 
};

In this example, to_wire() is used to send data in various scenarios - with no arguments, with one argument, and with multiple arguments.

The demo_to_wire_no_arg() function creates an IC_API instance, calls from_wire() without any arguments, and then calls to_wire(). This is a basic use case where no data is received or sent over the wire.

The demo_to_wire_one_arg() function receives and sends a single CandidTypeText object. The from_wire() function is called with this object, which then deserializes the incoming Candid byte stream and maps the value onto the hobby string. After creating a response message using the received hobby and the caller's principal, it calls to_wire() to send the response.

Lastly, the demo_to_wire_multiple_args() function handles a scenario where multiple arguments are received and send. It defines a CandidArgs object, appends the expected arguments to that, and calls from_wire() with this object. After constructing a response using the received hobby and the number of ICP earned from it, it calls to_wire() to send this response as a CandidArgs object.

This demonstrates how to_wire() can be effectively used to send data in a variety of use cases in your C++ methods.

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares the three query functions and specifies the Candid types each functions accepts as arguments and returns as values.


trap

Declaration:

// ic_api.h
class IC_API {
public:
  [[noreturn]] static void trap(const char *msg);
  [[noreturn]] static void trap(const std::string &msg); 
}

The trap method is a utility function that causes the canister's execution to be immediately terminated. It takes a string as an argument, which is used as the error message that's returned to the caller. This method can be called in situations where an unrecoverable error has occurred and you wish to halt execution and report an error to the user. The trap method accepts a C-style string (const char *) or a C++ string (const std::string &).

Example:

Here is how to use trap() in your C++ method:

/* file: src/demo_trap.cpp

$ icp canister call demo demo_trap '()' --environment local
Error: Failed query call.
Caused by: Failed query call.
  The Replica returned an error: code 5, message: "IC0503: Canister bkyz2-fmaaa-aaaaa-qaaaq-cai trapped explicitly:
  Hello expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae
  This is a trap demo."

*/
#include "demo_trap.h"
#include "ic_api.h"

void demo_trap() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  IC_API::trap("\n   Hello " + caller.get_text() + "\n   This is a trap demo.");
}
// file: src/demo_trap.h
#pragma once
#include "wasm_symbol.h"
void demo_trap() WASM_SYMBOL_EXPORTED("canister_query demo_trap");
// file: src/demo.did
service : {
    "demo_trap": () -> () query; 
};

In this example, the trap() method is used to halt the execution of the canister and return an error message to the caller. The demo_trap() method retrieves the CandidTypePrincipal of the caller using the get_caller() method, then it creates an error message and passes it to trap().

The demo.did file describes the interface of the service in the Candid Interface Description Language. It declares one query function, demo_trap, accepting no arguments and returning no values.


utilities

This section outlines various utility functions designed to streamline the process of interacting with data structures within C++ smart contracts developed using icpp-pro.


string_to_uint128_t and string_to_int128_t

Declaration:

// ic_api.h
class IC_API {
public:
  static std::optional<__uint128_t> string_to_uint128_t(const std::string &str);
  static std::optional<__int128_t> string_to_int128_t(const std::string &str); 
}

The string_to_uint128_t & string_to_int128_t functions serve to transform std::string values into __uint128_t and __int128_t format.

As clang++ compiler extensions, these types cannot be initialized from string literals, thus necessitating these custom functions.

Example:

/* file: src/demo_string_to_int128.cpp

$ icp canister call demo demo_string_to_int128 '()' --environment local
...nothing is printed here

-> check the console of the local network. The canister will print:
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] max__uint128_t=340282366920938463463374607431768211455
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] max__int128_t=170141183460469231731687303715884105727
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] min__int128_t=-170141183460469231731687303715884105727

-> when deployed to IC's mainnet, nothing is printed.

*/
#include "demo_string_to_int128.h"

#include <iostream>
#include <optional>
#include <string>

#include "ic_api.h"

void demo_string_to_int128() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // ----------------------------------------------------------------------------
  // Numbers larger than uint64_t & int64_t can NOT be initialized with a literal
  // - Use IC_API utility methods `string_to_uint128_t` & `string_to_int128_t`,
  //   to initialize them from a string.
  // - These utility methods return an `std::optional`, which succeeds only if the
  //   conversion is successful.

  std::optional<__uint128_t> max__uint128_t = IC_API::string_to_uint128_t(
      "340282366920938463463374607431768211455"); // 2^128 - 1
  std::optional<__int128_t> max__int128_t = IC_API::string_to_int128_t(
      "170141183460469231731687303715884105727"); // +2^127 - 1
  std::optional<__int128_t> min__int128_t = IC_API::string_to_int128_t(
      "-170141183460469231731687303715884105727"); // -2^127 - 1

  // Now we first check the content of the std::optional, before using to_string_128
  // This is a roundtrip from string -> value -> string
  if (max__uint128_t.has_value()) {
    std::cout << "max__uint128_t=" +
                     IC_API::to_string_128(max__uint128_t.value())
              << std::endl;
  } else {
    IC_API::trap("ERROR: string_to_uint128_t failed for max__uint128_t");
  }

  if (max__int128_t.has_value()) {
    std::cout << "max__int128_t=" + IC_API::to_string_128(max__int128_t.value())
              << std::endl;
  } else {
    IC_API::trap("ERROR: string_to_int128_t failed for max__int128_t");
  }

  if (min__int128_t.has_value()) {
    std::cout << "min__int128_t=" + IC_API::to_string_128(min__int128_t.value())
              << std::endl;
  } else {
    IC_API::trap("ERROR: string_to_int128_t failed for min__int128_t");
  }
  // ----------------------------------------------------------------------------

  ic_api.from_wire();
  ic_api.to_wire();
}
// file: src/demo_string_to_int128.h
#pragma once
#include "wasm_symbol.h"
void demo_string_to_int128()
    WASM_SYMBOL_EXPORTED("canister_query demo_string_to_int128");
// file: src/demo.did
service : {
    "demo_string_to_int128": () -> () query; 
};

to_string_128

Declaration:

// ic_api.h
class IC_API {
public:
  static std::string to_string_128(__uint128_t v);
  static std::string to_string_128(__int128_t v); 
}

The to_string_128 function serves to transform __uint128_t and __int128_t values into std::string format. As clang++ compiler extensions, these types are not handled by std::to_string, thus necessitating this custom function.

Example:

/* file: src/demo_to_string_128.cpp

$ icp canister call demo demo_to_string_128 '()' --environment local
...nothing is printed here

-> check the console of the local network. The canister will print:
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] v1 =101
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] v2 =102
  [Canister bkyz2-fmaaa-aaaaa-qaaaq-cai] v3 =-103

-> when deployed to IC's mainnet, nothing is printed.

*/
#include "demo_to_string_128.h"

#include <iostream>
#include <optional>
#include <string>

#include "ic_api.h"

void demo_to_string_128() {
  IC_API ic_api(CanisterQuery{std::string(__func__)}, false);
  CandidTypePrincipal caller = ic_api.get_caller();

  // -------------------------------------------------------------------------
  // Numbers smaller than uint64_t & int64_t can be initialized with a literal
  __uint128_t v1{101};
  __int128_t v2{102};
  __int128_t v3{-103};

  // To create a string, use the IC_API utility method `to_string_128`
  std::cout << "v1 =" + IC_API::to_string_128(v1) << std::endl;
  std::cout << "v2 =" + IC_API::to_string_128(v2) << std::endl;
  std::cout << "v3 =" + IC_API::to_string_128(v3) << std::endl;

  // ----------------------------------------------------------------------------
  // Numbers larger than uint64_t & int64_t can NOT be initialized with a literal
  // See `demo_string_to_int128.cpp`

  ic_api.from_wire();
  ic_api.to_wire();
}
// file: src/demo_to_string_128.h
#pragma once
#include "wasm_symbol.h"
void demo_to_string_128()
    WASM_SYMBOL_EXPORTED("canister_query demo_to_string_128");
// file: src/demo.did
service : {
    "demo_to_string_128": () -> () query; 
};

icpp.toml

The icpp.toml configuration file resides in the root of your project, and it defines the source files, and what compile & link flags to use.

You can select a different configuration toml file using the --config option

icpp-pro has the following build commands:

1. icpp build-wasm

  • builds a wasm to deploy to an IC canister
  • reads the [build-wasm] section of the icpp.toml
  • run icpp build-wasm --help for options
  • use icpp build-wasm --config TOML_FILE_NAME to use a different file for icpp.toml
  • use icpp build-wasm --to-compile mine-no-lib to skip building the libraries

2. icpp build-library

  • builds a static library for inclusion in the wasm
  • reads the [[build-library]] sections of the icpp.toml
  • run icpp build-library --help for options
  • use icpp build-library --config TOML_FILE_NAME to use a different file for icpp.toml

3. icpp build-native

  • builds a native debug executable, for interactive debugging in VS Code
  • reads the [build-native] section of the icpp.toml
  • run icpp build-native --help for options
  • use icpp build-native --config TOML_FILE_NAME to use a different file for icpp.toml
  • use icpp build-native --to-compile mine-no-lib to skip building the libraries

4. icpp build-library-native

  • builds a static library for inclusion in the native executable
  • reads the [[build-library-native]] sections of the icpp.toml
  • run icpp build-library-native --help for options
  • use icpp build-library-native --config TOML_FILE_NAME to use a different file for icpp.toml

The post-link pipeline

After the compiler and linker have done their work, icpp build-wasm runs a fixed sequence of steps. Two of them are worth understanding, because the order between them is a contract you can rely on:

link  ->  wasi2ic  ->  built-in globals-limit fix  ->  post_wasm_function  ->  metadata sections

The globals-limit fix (IC0505). The Internet Computer refuses to install a wasm module that defines more than 1000 globals, with the error IC0505. A wasi-sdk build of a large C++ project passes that limit easily - not because the code needs those globals, but because they are exported, and an exported global can never be considered dead. icpp-pro therefore removes the global exports and lets Binaryen's optimizer drop the globals that are now unused. For a real project this is the difference between 1048 globals and 1.

This runs by default, so a project that would have hit the limit simply works. Set fix_globals_limit = false in [build-wasm] to switch it off.

The backup. The fix writes build/<canister>_before_opt_internal.wasm before it touches anything. That file is the wasm as it was, which matters because the optimizer strips the wasm name section: run the optimized wasm under a tool like wasmtime and a backtrace shows <wasm function 42>, while the backup shows real function names. Keep it for debugging; never deploy it, since it still carries the exports the IC rejects.

The _internal in that name is deliberate. A hand-written post_wasm_function conventionally saves what it was handed as <canister>_before_opt.wasm, so the two steps use different names and both copies survive a build. They hold different stages, which is exactly what you want: _before_opt_internal is the wasm before the built-in fix, and a hook's _before_opt is the wasm as that hook received it.

Your own step runs second. post_wasm_function is called after the built-in fix, so what your function receives is always an already-valid wasm. Two notes follow:

  • You are free to keep your own backup under the conventional <canister>_before_opt.wasm; it will not collide with the built-in one.
  • If your function already applies the globals-limit fix, set fix_globals_limit = false rather than letting it run twice. Applying it twice is a semantic no-op but re-emits the binary, and on a large module that moves your wasm hash.

One subtlety if you care about reproducible builds: re-reading and re-writing a wasm through Binaryen can change the output bytes even when the pass does nothing, because its binary emitter is not a fixed point over a round-trip. Any custom pass you add may therefore move your wasm hash.

The content of the icpp.toml is best explained by this annotated example.

Note that for the lists of paths & dirs, you can use python glob operators, like "scr/*.cpp"

[build-wasm]
# The name of the canister, must match definition in icp.yaml
canister = "my_canister"
# The relative path to the candid file of the canister
did_path = "src/my_canister.did"
# A list of relative paths to the C++ files of the canister
cpp_paths = ["src/*.cpp", "src1/more/*.cpp"]
# A list of relative directory paths, where the compiler must look for C++ header files.
# - by default, the compiler already checks the directory where the toml file resides and the src sub-directory
cpp_include_dirs = ["src/vendors/*"]
# A list of additional C++ compile flags.
cpp_compile_flags = ["-D JSON_HAS_FILESYSTEM=0"]
# A list of additional link flags.
cpp_link_flags = []
# A list of relative paths to the C files of the canister
c_paths = []
# A list of relative directory paths, where the compiler must look for C header files.
# - by default, the compiler already checks the directory where the toml file resides and the src sub-directory
c_include_dirs = []
# A list of additional C compile flags.
c_compile_flags = []
# The built-in globals-limit fix (IC0505). On by default; see "The post-link
# pipeline" below. Set to false only if your own post_wasm_function already
# applies it - running it twice destroys the _before_opt backup.
fix_globals_limit = true
# hook: your own post-link step, run AFTER the built-in globals-limit fix
post_wasm_function = "scripts.optimize_wasm.main"
# Seldomly used - if non-empty, this will overwrite internal default compile & link flags
cpp_compile_flags_defaults = []
cpp_link_flags_defaults = []
c_compile_flags_defaults = []
[build-native]
# A list of relative paths to the additional C++ files to include in the native debug build.
# - the native debug build uses ALL the files listed in the [build-wasm] section, plus these
# - typically, it is just a main program with calls to canister methods, using the MockIC
cpp_paths = ["native/main.cpp"]
# A list of relative directory paths, where the compiler must look for C++ header files.
# - the native debug build uses ALL the include_dirs listed in the [build-wasm] section, pluse these
cpp_include_dirs = []
# A list of additional C++ compile flags.
# - the native debug build has it's own unique flags, and is NOT using the ones listed in the [build-wasm] section
cpp_compile_flags = []
# A list of additional link flags.
# - the native debug build has it's own unique flags, and is NOT using the ones listed in the [build-wasm] section
cpp_link_flags = []
# A list of relative paths to the additional C files to include in the native debug build.
# - the native debug build uses ALL the files listed in the [build-wasm] section, plus these
c_paths = []
# A list of relative directory paths, where the compiler must look for C header files.
# - the native debug build uses ALL the include_dirs listed in the [build-wasm] section, pluse these
c_include_dirs = []
# A list of additional C compile flags.
# - the native debug build has it's own unique flags, and is NOT using the ones listed in the [build-wasm] section
c_compile_flags = []
# Seldomly used - if non-empty, this will overwrite internal default compile & link flags
cpp_compile_flags_defaults = []
cpp_link_flags_defaults = []
c_compile_flags_defaults = []

[[build-library]]
# Name of the static library
lib_name = "libhello"
# Meaning of all fields below are same as described in [build-wasm]
cpp_paths = ["libhello/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []

[[build-library]]
# Name of the static library
lib_name = "libworld"
# Meaning of all fields below are same as described in [build-wasm]
cpp_paths = ["libworld/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []


hooks

icpp-pro has a build pipeline to go from C++ code to an Internet Computer compatible wasm:

cpp > objects > libs > wasi.wasm > wasm

You can write your own Python function, and tell icpp-pro about it in the icpp.toml project file.

An example can be found in canister_hooks:

# File structure:
|- icpp.toml            # the icpp-pro project file
|- scripts              # a folder with your custom scripts
|  |- __init__.py       # turns this folder into a python package
|  |- optimize_wasm.py  # your python module, with a fuction `main`
# file: icpp.toml
[build-wasm]
post_wasm_function = "scripts.optimize_wasm.main"

icpp-pro will then call your post_wasm_function at the appropriate time.


MockIC

The MockIC is a unique feature of icpp-pro. It allows you to test & interactively debug your C++ code using a native debug executable.

You use it in the following developer friendly workflow:

  • create a set of unit tests in a main program, typically native/main.cpp
  • build a native debug executable using the icpp build-native command
  • this creates the executable build-native/mockic.exe
  • debug it in VS Code, using CodeLLDB
  • you can also run it from the command line, which is recommended to do in a CI/CD pipeline

This approach accelerates development:

  • native debug builds are much faster than the optimized wasm builds
  • you can skip the deploy step
  • you can debug with VS Code, setting breakpoints and explore the data
  • you do NOT have to rely on debug_print statements, although you can use them as usual

Because the MockIC uses the exact same code for Candid decoding and encoding as when your code is running in a canister using WebAssembly, the data flow of messages coming in over the wire and going out to the wire is accurately modeled. You can build realistic test scenarios, and then debug them interactively.

We rely on this capability extensively and it increases development velocity tremendously. Being able to debug with breakpoints, explore the content of your data and step through the code as it runs is simply invaluable.

A good example how we used this capability is by looking at the native/main.cpp file for icpp_llama2, the backend canister of ICGPT. It would have been very difficult to port this LLM to the IC without native debug capability.

The way you use it is best explained by these code snipperts of the native/main.cpp file from our api-reference demo canister.

You need to pass in the serialized candid arguments as a hex string. For that, we recommend using didc.

// file: native/main.cpp

#include "main.h"

#include <iostream>

// include files declaring the canister methods we want to test
#include "../src/demo_candid_type_bool.h"
#include "../src/demo_trap.h"

// The Mock IC coming with icpp-pro
#include "mock_ic.h"

int main() {
  MockIC mockIC(true); // 'true' will debug_print

  // -----------------------------------------------------------------------------
  // Configs for the tests:

  // The pretend principal of the caller
  std::string my_principal{
      "expmt-gtxsw-inftj-ttabj-qhp5s-nozup-n3bbo-k7zvn-dg4he-knac3-lae"};

  bool silent_on_trap = true;

  // -----------------------------------------------------------------------------
  // Call a canister method and check the response:
  // Arguments to the run_test method of the mockIC:
  //   "demo_candid_type_bool" : name of the unit test
  //   demo_candid_type_bool   : the method to call:
  //                             -> must be defined in include above
  //                             -> mockIC internally uses a callback mechanism
  //   "4449444c00017e01"      : the hex string of the encoded argument.
  //   "4449444c00017e01"      : the hex string of the encoded expected result.
  //   silent_on_trap          : true  - when canister method traps, mockIC will not print the trap message
  //                             false - when canister method traps, mockIC will print the trap message
  //   my_principal            : a pretend principal of the caller
  //
  mockIC.run_test("demo_candid_type_bool", demo_candid_type_bool,
                  "4449444c00017e01", // didc encode '(true)'
                  "4449444c00017e01", // didc decode 4449444c00017e01
                  silent_on_trap, my_principal);

  // -----------------------------------------------------------------------------
  // Call a canister method and verify it traps:
  // Arguments to the run_trap_test method of the mockIC:
  //   "demo_trap"             : name of the unit test
  //   demo_trap               : the method to call:
  //                             -> must be defined in include above
  //                             -> mockIC internally uses a callback mechanism
  //   "4449444c0000"          : the hex string of the encoded argument.
  //   silent_on_trap          : true  - when canister method traps, mockIC will not print the trap message
  //                             false - when canister method traps, mockIC will print the trap message
  //   my_principal            : a pretend principal of the caller

  // Verify that a this method traps
  // '()' -> trap message
  mockIC.run_trap_test("demo_trap", demo_trap, "4449444c0000", silent_on_trap,
                       my_principal);

  // -----------------------------------------------------------------------------
  // prints a summary of the unit tests.
  // - returns 0 if all tests pass
  // - returns 1 if any tests failed
  return mockIC.test_summary();
}

Smoke Test

🚧 This documentation section is currently under construction! 🚧