Getting Started
Introduction to developing canisters in C++
Install icpp-pro
First install icpp-pro as explained in the Installation Guide
Create the greet
project
Create the greet
starter project with:
icpp init
This will create a complete C++ project, with:
demo script
In the root folder of the greet project, you can find two demo scripts:
demo.ps1
, for Windows PowerShelldemo.sh
, for Linux & Mac
You can run these scripts, as demonstrated in these short videos:
The sections below explain each step, and show how to run them from the command line.
Start local IC network
Start the local IC network with:
# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx start --clean --background
# In a Linux/Mac terminal
dfx start --clean --background
Build the WebAssembly file
You compile & link your code with:
icpp build-wasm
icpp will compile the C++ files defined in the icpp.toml
file and create a wasm file in the build
folder. The name of the wasm file to be created is defined inside icpp.toml
.
Deploy
Deploy the build/greet.wasm
to a canister in the local network with:
# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx deploy
# In a Linux/Mac terminal
dfx deploy
dfx deploys the files specified in dfx.json
file.
Working with static libraries
The greet project has a src folder plus two static libraries:
greet/
|- libhello/
| |- hello.cpp # a C++ file of the static library 'libhello'
| |- hello.h
|- libworld/
| |- world.cpp # a C++ file of the static library 'libworld'
| |- world.h
|- src/
| |- greet.cpp # a C++ file of the smart contract
| |- greet.h # include file used by greet.cpp
| |- greet.did # candid file, containing the canister API spec
The [[build-library]] sections in the icpp.toml
file specifies how the libraries must be build:
# file: icpp.toml
[[build-library]]
lib_name = "libhello"
cpp_paths = ["libhello/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []
[[build-library]]
lib_name = "libworld"
cpp_paths = ["libworld/*.cpp"]
cpp_include_dirs = []
cpp_compile_flags = []
c_paths = []
c_include_dirs = []
c_compile_flags = []
The following commands are available to work with these static libraries:
# Build everything, including the libraries
icpp build-wasm
# Once you have build all the libraries, and you only changed other code
# you can skip compiling the libraries
icpp build-wasm --to-compile mine-no-lib
# To build just a library. Omit LIBRARY_NAME to build all the libraries
icpp build-library [LIBRARY_NAME]
When you issue the icpp build-wasm
command, icpp will first compile & build a static library for each library defined in the icpp.toml
file. Each library will be build in it's own folder with name ./build-library/<lib_name/lib_name.a
. In our example:
greet/
|- build-library
| |- libhello
| | |- libhello.a
| |- libworld
| | |- libworld.a
Note that there will also be a static library for the internal IC and CANDID code, named __ic_candid__.a
. This is built automatically.
Then, icpp will create a wasm file in the build
folder. The name of the wasm file to be created is defined inside icpp.toml
. In our example:
greet/
|- build
| |- greet.wasm
Test
Test with dfx
You can test the APIs from the command line, using dfx canister call
.
For example, call the greet_2
API of your deployed greet
canister with:
# In a Windows PowerShell (MiniConda is recommended):
wsl --% . ~/.local/share/dfx/env; dfx canister --network local call greet greet_2 '("C++ Developer")'
# In a Linux/Mac terminal
dfx canister --network local call greet greet_2 '("C++ Developer")'
# The response will be:
(
"hello C++ Developer!\nYour principal is: .....-..etc..",
)
Test with Candid UI
You can also test the APIs from your browser, using Candid UI, just by clicking on the link printed by the dfx deploy
command.
Test automation with pytest
icpp-pro
comes with a smoke testing framework.
To run the automated API tests against your locally deployed canister, issue this command:
# from directory: `greet`
# Make sure the canister is deployed as described above.
# Then run the smoke tests using pytest
pytest --network local
=================== test session starts ===================
platform linux -- Python 3.10.8, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/<path>/icpp
plugins: anyio-3.6.2
collected 4 items
test/test_apis.py .. [100%]
==================== 4 passed in 0.13s ====================
Notes:
-
pytest
is installed automatically with icpp. -
pytest
will automatically detect the test folder:
greet/
|- test/
| |- conftest.py # pytest fixtures
| |- test_apis.py # the API tests
| |- __init__.py # empty file turning `test` into a python package
# -> required by pytest to resolve fixture imports
Debug with VS Code
icpp-pro
comes with a native development framework.
build-native
To create a native executable with the build in Mock IC, use the command:
icpp build-native
Run mockic.exe
You can run the native executable directly, with the command:
build-native/mockic.exe
Debug interactively
First, Install & Configure VS Code for use with icpp.
Then set breakpoints in your code and start the debugger.
The MockIC that was included in the executable will run the tests that you specified in native/main.cpp
and call your Smart Contract functions as if they're run in a canister.
This allows you to debug your Smart Contract interactively, not having to rely on debug_print statements alone. This speeds up your development process tremendously.
Next Steps
You can find additional Smart Contract examples in the icpp-demos repository.
IC Limitations
For the most part you can write any style of C++20, but the IC has two limitations to be aware of.
No file system
If your C++ code is making use of the file system, you will get an error message during compile time.
icpp uses the wasi-sdk compiler and sometimes, even when you are not using the file-system, the compiled Wasm module still includes functions that use the file-system and as a result it adds external system interfaces that the IC canister system API does not support. You will get an error during deployment to the canister.
To work around that issue, we have implemented traps to stub those functions. This allows you to compile & deploy your code without the system interfaces being added to the Wasm module, and if it so happens that you actually do use them, a trap with a clear message will occur at runtime.
1000 Globals
The IC has a hard limit allowing 1000 globals.
If your compiled Wasm module has more, then you will get this error message during deployment:
$ dfx deploy
...
Error: Failed while trying to deploy canisters.
Caused by: Failed while trying to deploy canisters.
Failed while trying to install all canisters.
Failed to install wasm module to canister 'greet'.
Failed to install wasm in canister 'rrkah-fqaaa-aaaaa-aaaaq-cai'.
Failed to install wasm.
The Replica returned an error: code 5, message:
"Wasm module of canister rrkah-fqaaa-aaaaa-aaaaq-cai is not valid:
Wasm module defined 2000 globals which exceeds the maximum number allowed 1000."
You can check how many globals are in your compiled Wasm module with these commands:
icpp build-wasm
wasm2wat <path-to-wasm> | grep "(global (;" | wc