Skip to main content Basic Types, Variables, and Constants | IoT Worker

Basic Types, Variables, and Constants

The first obstacle for a C programmer in Rust is not ownership. It is being able to write ordinary variables, constants, and basic types without fighting the language.

C code often starts like this:

#include <stdint.h>
#include <stdbool.h>

#define MAX_PACKET_SIZE 1024

int retry = 3;
uint32_t flags = 0;
bool enabled = true;
char tag = 'A';

Rust can express the same ideas, but the defaults are different:

const MAX_PACKET_SIZE: usize = 1024;

let mut retry: i32 = 3;
let flags: u32 = 0;
let enabled: bool = true;
let tag: u8 = b'A';

Struct layout, arrays, pointers, and strings all build on these basic types. Getting bindings, constants, and widths right first keeps later memory and API decisions from blurring together.

Variables are immutable by default

Local variables in C are mutable by default:

int retry = 3;
retry = retry - 1;

Rust bindings made with let are immutable by default:

let retry = 3;
retry = retry - 1; // compile error

Use mut when the value needs to change:

let mut retry = 3;
retry = retry - 1;

This is not just syntax. In C, whether a local variable changes later is often discovered by reading the whole scope. Rust writes that intent at the binding site: immutable by default, mutable when explicitly requested.

This helps library code stay readable. When you see:

let len = data.len();

you know len will not be reassigned. When you see:

let mut offset = 0;

you know it is likely a cursor or accumulated state.

Types can be written or inferred

C declarations must name a type:

int count = 0;
uint32_t flags = 0;

Rust can be explicit:

let count: i32 = 0;
let flags: u32 = 0;

It can also infer the type from context:

let count = 0;
let flags = 0_u32;

Without context, integer literals usually fall back to i32. In library code, protocol fields, and file formats, do not make readers guess important types. Write the type where it affects the contract.

A protocol version field is one byte:

let version: u8 = 1;

A length from a slice is a memory size:

let len: usize = data.len();

Data format fields and in-memory lengths are different concepts. The first should use explicit widths; the second is usually usize.

Prefer explicit integer widths

C code has two integer habits.

Traditional types:

int count;
long total;
unsigned flags;

And stdint.h types:

uint8_t version;
int32_t temperature;
uint64_t timestamp;

Rust integer names include signedness and width:

uint8_t   -> u8
uint16_t  -> u16
uint32_t  -> u32
uint64_t  -> u64

int8_t    -> i8
int16_t   -> i16
int32_t   -> i32
int64_t   -> i64

For binary protocols, file formats, register fields, and FFI structs, prefer fixed-width integers.

usize and isize are closer to C’s size_t and ssize_t style of platform-dependent sizes:

let index: usize = 0;
let len: usize = data.len();

They are right for in-memory lengths, indexes, and object sizes. They are not good defaults for network or file format fields.

bool is not an integer

C historically did not have a built-in bool; later code commonly uses _Bool through <stdbool.h>:

#include <stdbool.h>

bool enabled = true;

if (enabled) {
    /* ... */
}

C also commonly uses integers as conditions:

if (flags) {
    /* non-zero means true */
}

Rust’s bool is a separate type. Conditions must be bool:

let enabled: bool = true;

if enabled {
    // ...
}

Integers do not work as conditions:

let flags: u32 = 1;

if flags != 0 {
    // ...
}

This makes some C idioms longer, but the intent is clearer. if ptr, if len, and if flags become explicit checks in Rust.

Rust char is not C char

C char is usually a one-byte integer type:

char tag = 'A';

Rust char is a Unicode scalar value and is 4 bytes:

let ch: char = '中';

Use u8 for one byte:

let tag: u8 = b'A';

Use &str or String for text. Use CStr or CString for C strings. Do not replace C char with Rust char mechanically.

The first rule of thumb is:

C char            -> often one byte
Rust u8           -> one byte
Rust char         -> one Unicode scalar value
Rust &str/String  -> UTF-8 text

const is not C const

C has two common forms that look like constants.

One is a macro:

#define MAX_PACKET_SIZE 1024

Another is a const-qualified object:

const int max_packet_size = 1024;

Rust const is not the same as C const. It is closer to a typed compile-time constant value:

const MAX_PACKET_SIZE: usize = 1024;

Rust const has a type. It is not textual substitution, and it is not a read-only object with one fixed address. It is a good fit for array capacities, default limits, protocol constants, and state values.

Use static when a global storage location matters:

static APP_NAME: &str = "iot-tool";

The first distinction is:

const  -> compile-time constant value
static -> global value with a fixed storage location

Do not map this as C const -> Rust const. A better first mapping is:

C #define numeric constant   -> Rust const
C enum constant              -> Rust const / Rust enum
C const local object         -> Rust let
C static const global object -> Rust const or static, depending on whether a fixed address matters

Most numeric constants migrated from #define should start as Rust const.

Shadowing is not reassignment

C does not allow the same name to be defined twice in the same scope:

int len = 10;
int len = len + 1; // error

Rust allows shadowing:

let len = "1024";
let len: usize = len.parse().unwrap();

The second len does not modify the first one. It creates a new binding that shadows the old binding.

This is common when handling input:

let port = "1883";
let port: u16 = port.parse().unwrap();

The name represents the same business concept, but the type changes from text to number. Shadowing is better understood as a same-named new variable, not C-style reassignment.

Minimal migration table

The core mapping is:

C int              -> Rust i32 or choose i16/i64 as needed
C unsigned int     -> Rust u32 or choose u16/u64 as needed
C uint8_t          -> Rust u8
C size_t           -> Rust usize
C bool/_Bool       -> Rust bool
C char as byte     -> Rust u8
C char as text     -> Rust char / &str, depending on meaning
C #define value      -> Rust const
C const local object -> Rust let
C global object      -> Rust static
C mutable local      -> Rust let mut
C default local      -> Rust let

The important part is not memorizing names. It is separating three ideas:

  • external format fields use explicit-width types
  • in-memory lengths and indexes use usize
  • text, bytes, and characters are not one char * bucket

The next article on operators and casts will continue from C’s implicit conversions, integer promotion, and Rust’s explicit as.

Continue Reading