Joachim Breitner's Homepage
A Candid explainer: Language integration
This is the forth post in a series about the interface description language Candid.
Now for something completely different: How does Candid interact with the various host languages, i.e. the actual programming languages that you write your services and clients in?
There are two facets to that question:
How is the Candid interface represented inside the language?
Some languages with rich type systems can express all relevant information about a Candid method or service within its own type system, and then the concrete serialization/deserialization code can be derived from that type (e.g. using type classes in Haskell, Traits in Rust, or built into the compiler in Motoko).
Other languages have a less rich type system (e.g. C), no type-driven generic programming or are simply dynamically typed. In these cases, the Candid type has to be either transformed into specific code for that service by some external tool (e.g. JavaScript and TypeScript) or the Candid description has to be parsed and interpreted at runtime.
Either approach will give rise to a type mapping between the Candid types and the host language types. Developers will likely have to know which types correspond to which, which is why the Candid manual’s section on types explicitly lists that.
How is the Candid interface description produced and consumed?
This is maybe the even more important question; what comes first: The code written in the host language, or the Candid description. There are multiple ways to tackle this, all of which have their merits, so I let’s look at some typical approaches.
Generating candid from the host language
In many case you don’t care too much about the interface of your service, and you just want to write the functionality, and get the interface definition for free. This is what you get when you write Motoko services, where the compiler calculates the Candid interface based on the Motoko types of your actor methods, and the build tool (dfx
) puts that Candid file where it needs to go. You can thus develop services without ever writing or even looking at Candid type definitions.
The Candid library for Rust supports that mode as well, although you have to add some macros to your program to use it.
A downside of this model is that you have only indirect control over the generated Candid. Since it is type-driven, whenever there is more than one sensible Candid type for a given host language type, the translation tool has to make a choice, and if that does not suit you, that can be a problem.
In the case of Motoko we were able to co-design it with Candid, and their type systems are similar enough that this works well in practice. We have a specification of Candid-Motoko-type-mappings, and the type export from from Motoko to Candid is almost surjective. (Almost, because of Candid’s float32
type, which Motoko simply does not have, and because of service types with methods names that are not valid Motoko identifiers.)
Checking host language against Candid
The above works well when you (as the service developer) get to define the service’s interface as you go. But sometimes you want to develop a service that adheres to a given Candid interface. For example, in order to respond to HTTP requests in an Internet Computer service, you should provide a method http_request
that implements this interface (simplified):
type HeaderField = record { text; text; };
type HttpRequest = record {
method: text;
url: text;
headers: vec HeaderField;
body: blob;
};
type HttpResponse = record {
status_code: nat16;
headers: vec HeaderField;
body: blob;
};
service : {
http_request: (request: HttpRequest) -> (HttpResponse) query;
}
Here, a suitable mode of operation is to generate the Candid description of the service that you built, and then compare it against this expected interface with a tool that implements the Candid subtyping relation. This would then complain if what you wrote was not compatible with the above interface. The didc check
tool that comes with the Rust library can do that. If your service has to implement multiple such pre-defined interfaces, its actual interface will end up being a subtype of each of these interfaces.
Importing Candid
If you already have a Candid file, in particular if you are writing a client that wants to talk to an existing service, you can also import that Candid file into your language. This is a very common mode of operation for such interface descriptions languages (e.g. Protobuf). The details depend a lot on the host language, though:
In Motoko, you can import typed actor references: Just write
import C "canister:foo"
wherefoo
is the name of another canister in your project, and your build tool (dfx
) will pass the Candid interface offoo
to the Motoko compiler, which then translates that Candid service type into a Motoko actor type for you to use.The mapping from Candid types to Motoko types is specified in
IDL-Motoko.md
as well, via the functioni(…)
.The vision was always that you can import a reference to any canister on the Internet Computer this way (
import C "ic:7e6iv-biaaa-aaaaf-aaada-cai"
), and the build tool would fetch the interface automatically, but that has not been implemented yet, partly because of disagreement about how canisters expose their interface (see below). The Motoko compiler is ready, though, as described in its build tool interface specification.What is still missing in Motoko is a way to import a Candid type alone (without a concrete canister reference), to use the types somewhere (in function arguments, or to assert the type of the full canister).
The Rust library does not support importing Candid, as far as I know. If you write a service client in Rust, you have to know which Rust types map to the right Candid types, and manually get that right.
For JavaScript and TypeScript, the
didc
tool that comes with the Rust library can take a Candid file and produce JS resp. TS code that gives you an object representing the service with properly typed methods that you can easily interact with.With the Haskell Candid library you can import Candid inline or from a file, and it uses metaprogramming (Template Haskell) to generate suitable Haskell types for you, which you can encode/decode at. This is for example used in the test suite of the Internet Identity, which executes that service in a simulated Internet Computer environment.
Generic and dynamic use
Finally, it’s worth mentioning that Candid can be used generically and dynamically:
Since Canisters can indicate their interface, a website like ic.rocks that enumerates Canisters can read that interface, and provide a fully automatically generated UI for them. For example, you can not only see the Candid interface of the Internet Identity canister at https://ic.rocks/principal/rdmx6-jaaaa-aaaaa-aaadq-cai, but actually interact with it!
Similarly, during development of a service, it is very useful to be able to interact with it before you have written your actual front-end. The Candid UI tool provides that functionality, and can be used during local development or on-chain. It is also integrated into the Internet Computer playground.
Command line tools like
dfx
allow you to make calls to services, even without having their actual Candid interface description around. However, such dynamic use was never part of the original design of Candid, so it is a bit rough around the edges – the textual format is a bit unwieldy, you need to get the types right, and field name in the responses may be missing.
Do you want to know why field names are missing then? Then check out the next and final post of this series, where I discuss a number of Candid’s quirks.
Have something to say? You can post a comment by sending an e-Mail to me at <mail@joachim-breitner.de>, and I will include it here.