Language Atlas (mini-courses for more languages) Module 13 – Crystal for Ruby developers
Crystal macros, fibers and C bindings
Write Crystal macros that generate methods and read their expansion, pass values between fibers with channels and select, and call C through lib and fun.
What you will learn
- Write a macro that generates methods while the program compiles, and read its expansion with crystal tool expand
- Predict when fibers run and wait as they pass values through channels with and without room
- Build a pool of fibers that shares work, and wait for the first answer with select and a timeout
- Declare C functions with lib and fun, and say which strings and callbacks C cannot take
Before you start
What you’ll learn · Before you start
What you will learn
- Write a macro that generates methods while the program compiles, and read its expansion with crystal tool expand
- Predict when fibers run and wait as they pass values through channels with and without room
- Build a pool of fibers that shares work, and wait for the first answer with select and a timeout
- Declare C functions with lib and fun, and say which strings and callbacks C cannot take
Before you start
On this page
A macro of seven lines in units.cr writes three conversion methods while the program compiles, and
crystal tool expand prints the code it wrote. Three fibers on one thread send three squares to the main fiber, and
two of them never print their last line: concurrency in Crystal follows rules you can predict from the run.
This lesson covers the three things Crystal adds to a Ruby developer’s toolbox: macros that write code before the program runs, fibers and channels that share one thread, and lib blocks that call C functions directly, with the strings and the callbacks that C cannot take.
Macros write code while the program compiles
units.cr needs one conversion method per unit. Instead of typing three methods, it calls a
macro (Crystal): Code that runs inside the compiler and writes Crystal code where it is called. Its arguments arrive as pieces of the program, {% %} runs loops and conditions, and {{ }} pastes values into the code it writes., define_units, which writes them:
# A macro that writes one conversion method per unit while the program compiles.
macro define_units(base, **factors)
{% for unit, factor in factors %}
def self.{{unit.id}}_to_{{base.id}}(value : Float64) : Float64
value * {{factor}}
end
{% end %}
end
module Length
define_units metre, inch: 0.0254, foot: 0.3048, yard: 0.9144
end
puts Length.inch_to_metre(12.0)
puts Length.foot_to_metre(10.0)
puts Length.yard_to_metre(100.0)
p {{ Length.class.methods.map(&.name.stringify).sort }} A macro runs inside the compiler. Its arguments arrive as pieces of the program, not as values: base is the name
metre, and factors holds three named number literals. Code between {% and %} runs while compiling, so the loop
runs three times, and {{ }} pastes a value into the code the macro writes. Step through the call:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- A macro call. module Length calls define_units with metre and three named arguments. The call looks like a method call, but it is handled while the program compiles, before any value exists.
- The arguments are syntax. The macro receives its arguments as pieces of the program: base is the name metre, and factors holds inch: 0.0254, foot: 0.3048 and yard: 0.9144 as number literals.
- The loop runs in the compiler. The {% for unit, factor in factors %} loop is macro code: it runs three times inside the compiler, once for each named argument, and writes one method each time.
- Values pasted into code. {{unit.id}} and {{base.id}} paste the names, {{factor}} the number. The first pass writes def self.inch_to_metre(value : Float64) : Float64 with value * 0.0254 as its body.
- Ordinary methods. crystal tool expand printed the three methods the macro wrote. From here the compiler checks them like any code: their types, their calls, their names.
- A name the macro did not write. typo.cr calls Length.feet_to_metre. The macro wrote foot_to_metre, so the compiler stops with undefined method and asks Did you mean foot_to_metre.
- The program runs. The program calls the methods as if they had been typed in: 12 inches gave 0.30479999999999996 metres, 10 feet 3.048 and 100 yards 91.44.
from session import run
# define_units writes three methods while units.cr compiles; tool expand shows the code it wrote; a name it did not
# write is a compile error.
run("""
cd macros
crystal run units.cr
crystal tool expand -c units.cr:11:3 units.cr
crystal run accessors.cr
!crystal run typo.cr
""") Output
$ cd macros
$ crystal run units.cr
0.30479999999999996
3.048
91.44
["foot_to_metre", "inch_to_metre", "yard_to_metre"]
$ crystal tool expand -c units.cr:11:3 units.cr
1 expansion found
expansion 1:
define_units(metre, inch: 0.0254, foot: 0.3048, yard: 0.9144)
# expand macro 'define_units' (units.cr:2:1)
~> def self.inch_to_metre(value : Float64) : Float64
value * 0.0254
end
def self.foot_to_metre(value : Float64) : Float64
value * 0.3048
end
def self.yard_to_metre(value : Float64) : Float64
value * 0.9144
end
$ crystal run accessors.cr
C7: 3 guests
["guests", "guests=", "initialize", "seat"]
$ crystal run typo.cr
Showing last frame. Use --error-trace for full trace.
In typo.cr:14:13
14 | puts Length.feet_to_metre(10.0)
^------------
Error: undefined method 'feet_to_metre' for Length:Module
Did you mean 'foot_to_metre'?
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 macros_session.py
crystal tool expand -c units.cr:11:3 names the line and column of the call and printed the three methods the macro
wrote, exactly as if they had been typed. The last line of units.cr runs macro code too:
{{ Length.class.methods.map(&.name.stringify).sort }} listed the class methods of Length while compiling, and the
program printed that list. Ruby developers know the
reflection the guide for Rubyists says Crystal leaves out, such as methods; this is where Crystal does it, at
compile time.
You have used macros already. property and getter in accessors.cr are macros of the standard library: the
compiler’s list of Booking’s methods shows guests and guests= for property guests, and seat alone for
getter seat. And a name that no macro wrote does not exist: typo.cr calls feet_to_metre, the compiler stops with
undefined method 'feet_to_metre', and it suggests the method the macro did write, foot_to_metre.
See what a macro wrote
Give crystal tool expand -c file.cr:line:column file.cr the line and column of a macro call. It is the quickest
way to see why a generated method has the wrong name or type.
Checkpoint
Check: Macros that write methods while compiling
Fibers take turns on one thread, and channels hand values over
spawn starts a fiber: A lightweight unit of execution that Crystal's scheduler runs, started with spawn. Fibers take turns on a thread: one runs until it waits on a channel, input, output or a sleep, or until it ends., a lightweight unit of execution that Crystal’s scheduler runs. The
concurrency guide states the rule that explains every output in this section: a program runs one fiber at a time by
default, and fibers are cooperative, so a fiber keeps the thread until it waits, on a channel, input, output or a
sleep, or ends. round_trip.cr sends three squares through a channel (Crystal): A typed queue that fibers send values through. Channel(Int32).new has no room, so send waits until a fiber receives; Channel(Int32).new(3) has room for three values, so send goes on until it is full. with no room in
it, Channel(Int32).new:
# Three fibers each send one square through an unbuffered channel; the main fiber receives three times.
squares = Channel(Int32).new
3.times do |i|
spawn do
puts "fiber #{i} sends #{i * i}"
squares.send(i * i)
puts "fiber #{i} is done"
end
end
puts "main waits"
got = Array.new(3) { squares.receive }
puts "main got #{got}" from session import run
# Three fibers send one square each to the main fiber, through a channel without room and through one with room for
# three: the order of the lines shows when each fiber runs and when it waits.
run("""
cd fibers
crystal run round_trip.cr
crystal run round_trip_buffered.cr
""") Output
$ cd fibers $ crystal run round_trip.cr main waits fiber 0 sends 0 fiber 0 is done fiber 1 sends 1 fiber 2 sends 4 main got [0, 1, 4] $ crystal run round_trip_buffered.cr main waits fiber 0 sends 0 fiber 0 is done fiber 1 sends 1 fiber 1 is done fiber 2 sends 4 fiber 2 is done main got [0, 1, 4]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fibers_session.py
The first run printed fiber 0 is done and never the same line for fibers 1 and 2. Step through it in the order of
its lines:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Spawn three fibers. spawn puts each fiber in the queue of fibers ready to run. None of them runs yet: the main fiber keeps going.
- main waits. main prints main waits and calls receive. The channel holds nothing, so main blocks, and the scheduler runs the first fiber in the queue.
- Fiber 0 hands its value over. Fiber 0 prints fiber 0 sends 0 and calls send. main is already waiting, so the value goes straight to it and fiber 0 does not wait.
- Fiber 0 finishes. Fiber 0 goes on, prints fiber 0 is done and ends. A fiber gives up the thread only when it waits or ends.
- Fibers 1 and 2 wait. Fiber 1 prints fiber 1 sends 1, but main has not come back to receive yet, so send blocks. Fiber 2 prints fiber 2 sends 4 and blocks the same way.
- main takes 1 and 4. main runs again with 0 and calls receive twice more, taking 1 from fiber 1 and 4 from fiber 2.
- The program ends. main prints main got [0, 1, 4] and ends, and the program ends with it. Fibers 1 and 2 were ready to run again, but they never printed fiber 1 is done or fiber 2 is done.
A send on a channel without room waits until a fiber receives. Fiber 0 found main already waiting and went on; fibers 1 and 2 found nobody and waited. By the time main had taken their values it was the last thing to run, and a Crystal program ends when its main fiber does, whatever other fibers still have to do.
The second run made one change, Channel(Int32).new(3), a channel with room for three values. Now no send had to
wait, so each fiber printed both of its lines before the next one started, and main found the 1 and the 4 waiting in
the channel. The values were the same, [0, 1, 4]; the order of everything else changed with the capacity.
Key idea
On one thread a fiber runs until it waits or ends: a send on a channel without room waits for a receiver, a channel with room lets the sender go on, and the program ends when the main fiber does.
Checkpoint
Check: Fibers, channels and the order of a run
A pool of fibers shares the work, and select takes the first answer
pool.cr checks the status of six hosts with three worker fibers. The statuses come from a table in the program, so the run needs no network; a real checker would make a request where the table is read, and each wait for an answer would let another fiber run:
# A status checker: three worker fibers take hosts from one channel and send results to another.
# The statuses come from a table in the program, so the run needs no network.
STATUS = {
"api.local" => 200,
"billing.local" => 503,
"cdn.local" => 200,
"docs.local" => 404,
"mail.local" => 200,
}
record Result, index : Int32, host : String, status : Int32?, worker : Int32
hosts = ["api.local", "billing.local", "cdn.local", "docs.local", "mail.local", "wiki.local"]
jobs = Channel({Int32, String}).new
results = Channel(Result).new
3.times do |worker|
spawn do
while job = jobs.receive?
index, host = job
results.send(Result.new(index, host, STATUS[host]?, worker))
end
end
end
spawn do
hosts.each_with_index { |host, index| jobs.send({index, host}) }
jobs.close
end
arrived = Array.new(hosts.size) { results.receive }
arrived.each { |r| puts "worker #{r.worker}: #{r.host}" }
puts "-- in input order --"
arrived.sort_by(&.index).each do |r|
puts "#{r.host}: #{r.status || "unknown host"}"
end The status checker: two channels between three kinds of fiber
Text description of the diagram
The diagram is read from top to bottom.
- A sender fiber sends each of the six hosts, with its index in the list, into the jobs channel, then closes the channel.
- jobs is a Channel of tuples, {Int32, String}: an index and a host name.
- Three worker fibers each call receive? on jobs until it returns nil, which happens once the channel is closed and empty. For each job they look the host up in the STATUS table, which gives nil for a host that is not in it.
- Each worker sends a Result, holding the index, the host, the status and the worker's number, into the results channel.
- The main fiber receives six results, in the order they arrive, and sorts them by index to print the report in the order of the input.
The sender fiber closes jobs after the last host. receive? gives nil once a channel is closed and empty, which
ends each worker’s while loop, and the record Result carries the index of its host:
from session import run
# A pool of three worker fibers over six hosts whose statuses come from a table in the program, and select with a
# timeout.
run("""
cd fibers
crystal run pool.cr
crystal run select_timeout.cr
""") Output
$ cd fibers $ crystal run pool.cr worker 0: api.local worker 0: docs.local worker 1: billing.local worker 2: cdn.local worker 0: mail.local worker 0: wiki.local -- in input order -- api.local: 200 billing.local: 503 cdn.local: 200 docs.local: 404 mail.local: 200 wiki.local: unknown host $ crystal run select_timeout.cr cache answered no answer within 200 ms
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pool_session.py
Worker 0 handled four of the six hosts. Each time it sent a result it went straight back to jobs, while workers 1
and 2 were still waiting to hand their results over: the order of arrival comes from the scheduler, not from the
input, and with parallelism switched on it would change from run to run. That is why each result carries its index
and main sorts by it: the report in input order is the same whoever did the work. wiki.local is not in the table, so
STATUS[host]? gave nil and the report says unknown host.
select_timeout.cr waits on two channels and a timeout at once. In the first round the fiber that stands for the cache
sent its answer while select waited, so select took it; in the second nothing came, and after 200 milliseconds the
timeout branch ran. A checker uses the same pattern to give up on a host that does not answer.
Checkpoint
Check: A pool of fibers, and select with a timeout
lib and fun call C directly, with rules for strings and callbacks
A lib block: A Crystal declaration that names C functions with their C types, such as fun strlen(s : UInt8*) : LibC::SizeT inside lib LibText, so that the program can call them like class methods. declares C functions with their C types, and the program then calls
them like class methods. The C library is linked into every program already, so these need no @[Link]
annotation:
cbind/libc.cr
# C functions of the system library, declared in lib blocks and called from Crystal.
lib LibText
fun strlen(s : UInt8*) : LibC::SizeT
fun toupper(c : Int32) : Int32
end
lib LibMath
fun cbrt(x : Float64) : Float64
end
puts LibText.strlen("chai")
puts LibText.toupper('q'.ord).chr
puts LibMath.cbrt(27.0)
hindi = "चाय"
puts "#{hindi}: size #{hindi.size}, bytesize #{hindi.bytesize}, strlen #{LibText.strlen(hindi)}"
order = "tea\0coffee"
puts "order: size #{order.size}, bytesize #{order.bytesize}, strlen #{LibText.strlen(order)}"
begin
order.check_no_null_byte
rescue e : ArgumentError
puts "check_no_null_byte: #{e.message}"
end cbind/qsort.cr
# qsort from the C library sorts a Crystal array in place, calling back into a Crystal proc.
lib LibSort
fun qsort(base : Void*, count : LibC::SizeT, size : LibC::SizeT, compare : (Void*, Void*) -> Int32)
end
marks = [72, 18, 95, 43, 60]
LibSort.qsort(marks.to_unsafe.as(Void*), marks.size, sizeof(Int32), ->(a : Void*, b : Void*) {
a.as(Int32*).value <=> b.as(Int32*).value
})
p marks cbind/closure.cr
# A proc that uses a local variable is a closure, and C cannot hold one.
lib LibSort
fun qsort(base : Void*, count : LibC::SizeT, size : LibC::SizeT, compare : (Void*, Void*) -> Int32)
end
marks = [72, 18, 95, 43, 60]
descending = true
LibSort.qsort(marks.to_unsafe.as(Void*), marks.size, sizeof(Int32), ->(a : Void*, b : Void*) {
order = a.as(Int32*).value <=> b.as(Int32*).value
descending ? -order : order
})
p marks from session import run
# C functions called from Crystal: strlen, toupper and cbrt, qsort with a Crystal callback, and a callback C refuses.
run("""
cd cbind
crystal run libc.cr
crystal run qsort.cr
!crystal run closure.cr
""") Output
$ cd cbind
$ crystal run libc.cr
4
Q
3.0
चाय: size 3, bytesize 9, strlen 9
order: size 10, bytesize 10, strlen 3
check_no_null_byte: String contains null byte
$ crystal run qsort.cr
[18, 43, 60, 72, 95]
$ crystal run closure.cr
Showing last frame. Use --error-trace for full trace.
In closure.cr:8:69
8 | LibSort.qsort(marks.to_unsafe.as(Void*), marks.size, sizeof(Int32), ->(a : Void*, b : Void*) {
^
Error: can't send closure to C function (closured vars: descending)
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cbind_session.py
The types are C’s, written the Crystal way. C’s char * is UInt8*, a pointer to bytes, and a String passed for it
goes as a pointer to its bytes, through its to_unsafe. C counts bytes, not characters: चाय, tea in Hindi, has 3
characters and 9 bytes in UTF-8, and strlen said 9. It also stops at the first zero byte, so for "tea\0coffee" it
said 3 where bytesize said 10. A C function given that string sees only tea; check_no_null_byte raises an
ArgumentError for such a string, so call it before you pass text from outside the program to C.
qsort.cr sorts a Crystal array with the C library’s qsort, which calls a Crystal proc back to compare two elements.
Step through the call:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Crystal calls qsort. qsort.cr passes four things: a pointer to the array's first Int32, marks.to_unsafe, the count 5, the size of one element, 4, and a proc that compares two elements.
- C calls the proc. qsort knows nothing about Int32: it hands the proc two Void* pointers into the array, and the proc reads the two values through them.
- The proc answers. The proc returns a.as(Int32*).value <=> b.as(Int32*).value: a negative number, 0 or a positive number. qsort swaps elements from those answers and calls it again.
- The array is sorted in place. qsort returns, and the Crystal array it was given is sorted: p marks printed [18, 43, 60, 72, 95].
- A proc C cannot take. closure.cr's proc reads descending, a local variable of the program, so it is a closure. A C function pointer has no room for that variable, and the compiler refuses: can't send closure to C function.
The proc in closure.cr reads descending, a local variable, so it is a closure: A function, such as a Crystal proc, that uses variables of the code around it and carries them along. A C function pointer has no room for those variables, so Crystal refuses to pass a closure to C., and C has
no room for the variable it carries: the compiler said can't send closure to C function (closured vars: descending). The callbacks reference shows the way out for C functions that take a user-data pointer: pass a proc
without a closure and send the data through that pointer instead.
Pointers switch Crystal's checks off
The Pointer reference calls it the only unsafe type in Crystal: a pointer does not know how much memory after it is
valid, so reading past an array through one is never stopped. Keep pointer code in a small wrapper around the C
call, check lengths there, and use a Slice, a pointer with a size and bounds checks, wherever Crystal code reads
the memory.
Checkpoint
Check: C functions through lib and fun
Practise: macro output and C strings, in Python
Both steps of the lab rebuild something this lesson ran. The first writes the methods that define_units wrote, as
text, and its sample tests compare your text with what crystal tool expand printed; the second works out what a C
function sees of a Crystal string. Each step has starter code, sample tests and a first hint:
Code lab · 2 steps
Macro output and C strings, in Python
Write the methods define_units writes for any units, and the size, bytesize and strlen of a Crystal string, so that both match what the sessions printed.
Exercise · Easy · Python
Write the methods define_units writes
A macro is code that writes code. Write the same generator in Python: define_units(base, factors) in expand.py returns, as one string, the Crystal methods that the lesson's define_units macro writes.
baseis the unit converted to, such as"metre";factorsis a list of(unit, factor)pairs, the factor as text, such as[("inch", "0.0254"), ("foot", "0.3048")].- Each pair gives three lines:
def self.<unit>_to_<base>(value : Float64) : Float64, then the bodyvalue * <factor>indented by two spaces, thenend. - The methods come in the order of
factors, with one empty line between two methods and no line break after the lastend. No pairs give an empty string.
The sample tests hold the three methods that crystal tool expand printed for metre and the lesson's three units, without the ~> marker and the indentation that the tool adds when it shows them.
Starter code · expand.py
def define_units(base, factors):
"""The Crystal methods that define_units writes for base and the (unit, factor) pairs."""
code = ""
for unit, factor in factors:
code += f"def {unit}_to_{base}(value)\n value * {factor}\nend\n"
return code The sample tests · test_expand.py
from expand import define_units
LENGTH = """def self.inch_to_metre(value : Float64) : Float64
value * 0.0254
end
def self.foot_to_metre(value : Float64) : Float64
value * 0.3048
end
def self.yard_to_metre(value : Float64) : Float64
value * 0.9144
end"""
def test_the_lessons_three_units():
"""what crystal tool expand printed for units.cr"""
factors = [("inch", "0.0254"), ("foot", "0.3048"), ("yard", "0.9144")]
assert define_units("metre", factors) == LENGTH
def test_one_unit():
"""a single method, with no empty line after it"""
assert define_units("gram", [("ounce", "28.349523125")]) == (
"def self.ounce_to_gram(value : Float64) : Float64\n value * 28.349523125\nend"
)
def test_order_follows_the_factors():
"""the methods come in the order of the pairs"""
code = define_units("litre", [("pint", "0.568261"), ("cup", "0.25")])
assert code.index("pint_to_litre") < code.index("cup_to_litre")
assert code.count("\n\n") == 1
def test_no_units():
"""no pairs, no methods"""
assert define_units("metre", []) == "" A hint
Build one string per method with an f-string of three lines joined by "\n", collect them in a list, and join the list with "\n\n", which puts exactly one empty line between two methods.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Exercise · Easy · Python
Work out what C sees of a Crystal string
A Crystal string is UTF-8 text, and C reads it as bytes up to the first zero byte. Write two functions in lengths.py:
lengths(text): a tuple of the three numbers the lesson printed for a string: itssizein characters, itsbytesizein UTF-8 bytes, and what C'sstrlenreturns for it, the number of bytes before the first zero byte.safe_for_c(text):Truewhen the string holds no zero byte, so thatcheck_no_null_bytewould let it through and a C function sees all of it.
The sample tests start with the lesson's strings: "chai", चाय and "tea\0coffee".
Starter code · lengths.py
def lengths(text):
"""(size, bytesize, strlen) of a Crystal string that holds text."""
return (len(text), len(text), len(text))
def safe_for_c(text):
"""True when a C function would see the whole string."""
return True The sample tests · test_lengths.py
from lengths import lengths, safe_for_c
def test_the_lessons_strings():
"""chai, चाय and tea\\0coffee, as libc.cr printed them"""
assert lengths("chai") == (4, 4, 4)
assert lengths("चाय") == (3, 9, 9)
assert lengths("tea\0coffee") == (10, 10, 3)
def test_more_characters():
"""a cup of tea in one character, three bytes; an empty string"""
assert lengths("☕") == (1, 3, 3)
assert lengths("") == (0, 0, 0)
assert lengths("\0") == (1, 1, 0)
def test_zero_bytes_are_unsafe_for_c():
"""check_no_null_byte refuses a string with a zero byte"""
assert safe_for_c("चाय") is True
assert safe_for_c("tea\0coffee") is False
assert safe_for_c("") is True A hint
text.encode("utf-8") gives the bytes Crystal keeps, and len() of it is the bytesize. For strlen, find where the first zero byte is in those bytes: bytes.find(b"\0") gives -1 when there is none.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the self-check on the four ideas of the lesson. It asks about the idea your answers show least first, and every answer comes with the reason:
Practice that adapts to you
Practise this lesson
11 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (mid-level to senior): When do you write a Crystal macro instead of a method?
When the code to write depends on the program itself rather than on values: methods named after the units of a list,
accessors for instance variables, or code that needs the program’s types. A macro runs inside the compiler, takes its
arguments as syntax, and writes code that the compiler then checks like any other, so a mistake in what it writes
shows up as a compile error with a name, as feet_to_metre did. A method runs while the program runs and takes
values. Prefer a method whenever one can do the job: it is easier to read, to test and to debug. When you do write a
macro, crystal tool expand shows exactly what it wrote, and the standard library’s property, getter and record
are the models to follow.
Two longer questions, on a C call that stalls a server and on a pool whose report changes between machines:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to staff
A Crystal service handles many requests at once with fibers. One handler calls a C library function through a lib block, and the call takes about two seconds. While it runs, every other request stalls. Why, and what are your options?
-
Fresher to senior
A status checker built as a pool of fibers prints its report in a different order on a teammate's machine, and a test that compares the report with a saved file fails there but passes on yours. What is going on, and how do you fix both the program and the test?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
When does a Crystal macro run, and what are its arguments?
Inside the compiler, while the program compiles. Its arguments are pieces of the program, such as the name metre, not values; {% %} runs loops and conditions and {{ }} pastes values in.
-
How do you see the code a macro call wrote?
crystal tool expand -c file.cr:line:column file.cr, giving the line and column of the call. It prints the generated methods as if they had been typed.
-
How do Crystal fibers share one thread?
Cooperatively: one runs at a time and keeps the thread until it waits on a channel, input, output or a sleep, or until it ends.
-
Why did fibers 1 and 2 of round_trip.cr never print their last line?
A send on a channel with no room waits for a receiver. They waited, main took their values and finished, and a program ends with its main fiber, whatever the others still have to do.
-
What changed when round_trip.cr's channel was given room for three values?
No send had to wait, so each fiber printed both lines before the next started. The values were still [0, 1, 4]; the order of everything else changed.
-
Why does each result of the worker pool carry its host's index?
Results arrive in the scheduler's order, not the input's, so main sorts by index and the report reads in input order whoever did the work.
-
What does C's strlen return for the Crystal strings चाय and "tea\0coffee"?
9 and 3: C counts bytes and stops at the first zero byte. check_no_null_byte raises an ArgumentError for such a string before it reaches C.
-
Why was a proc that reads the local variable descending refused as a qsort callback?
It is a closure, and a C function pointer has no room for the variables it carries. Pass a proc without a closure, and send data through a user-data pointer when C offers one.
Key takeaways
- A macro runs inside the compiler, takes its arguments as syntax, loops with
{% %}and pastes with{{ }}; the methods it writes are checked like any code, andcrystal tool expandshows them. property,getterandrecordare macros of the standard library, and macro code can read the program’s own methods while compiling.- Fibers take turns on one thread: a fiber runs until it waits or ends, a send on a channel without room waits for a receiver, and the program ends with its main fiber.
- In a pool, results arrive in the scheduler’s order, so they carry an index;
receive?ends the workers once the jobs channel is closed, andselectwithtimeoutgives up on an answer that does not come. - A lib block declares C functions with C types: strings go as bytes and stop at the first zero byte, and a proc that is a closure cannot be passed to C.
Terms in this lesson
- channel (Crystal)
- A typed queue that fibers send values through. Channel(Int32).new has no room, so send waits until a fiber receives; Channel(Int32).new(3) has room for three values, so send goes on until it is full.
- closure
- A function, such as a Crystal proc, that uses variables of the code around it and carries them along. A C function pointer has no room for those variables, so Crystal refuses to pass a closure to C.
- fiber
- A lightweight unit of execution that Crystal's scheduler runs, started with spawn. Fibers take turns on a thread: one runs until it waits on a channel, input, output or a sleep, or until it ends.
- lib block
- A Crystal declaration that names C functions with their C types, such as fun strlen(s : UInt8*) : LibC::SizeT inside lib LibText, so that the program can call them like class methods.
- macro (Crystal)
- Code that runs inside the compiler and writes Crystal code where it is called. Its arguments arrive as pieces of the program, {% %} runs loops and conditions, and {{ }} pastes values into the code it writes.
References
- Macros (Crystal 1.21) (Crystal)
- Using the compiler: crystal tool expand (Crystal 1.21) (Crystal)
- Object: the property and getter macros (Crystal 1.21.1 API) (Crystal)
- Concurrency (Crystal 1.21) (Crystal)
- Parallelism and execution contexts (Crystal 1.21) (Crystal)
- Channel (Crystal 1.21.1 API) (Crystal)
- C bindings: lib (Crystal 1.21) (Crystal)
- C bindings: fun (Crystal 1.21) (Crystal)
- C bindings: callbacks (Crystal 1.21) (Crystal)
- Pointer (Crystal 1.21.1 API) (Crystal)
- String: to_unsafe and check_no_null_byte (Crystal 1.21.1 API) (Crystal)
- qsort: sort a table of data (The Open Group)
- strlen: get length of fixed size string (The Open Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress