-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmemory_management.exs
More file actions
223 lines (166 loc) · 9.26 KB
/
Copy pathmemory_management.exs
File metadata and controls
223 lines (166 loc) · 9.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# Firebird Memory Management Example
# Run: mix run examples/memory_management.exs
#
# This example demonstrates Firebird.Memory — the structured memory allocator
# for passing complex data (strings, arrays, structs) to and from WASM modules.
#
# When do you need this?
# - WASM functions that accept/return strings (ptr + len pairs)
# - Passing arrays of numbers to WASM for batch processing
# - Interoperating with Go/Rust/C structs in WASM linear memory
# - Any time you go beyond simple integer arguments
alias Firebird.Memory
IO.puts("🔥 Firebird Memory Management\n")
# ─── Setup ──────────────────────────────────────────────────────────────────
# Load any WASM module — we use the bundled sample for demonstration.
# The Memory allocator works with ANY WASM instance that has linear memory.
wasm = Firebird.load!(Firebird.sample_wasm_path())
IO.puts("✓ Loaded WASM instance\n")
# ─── 1. Creating an Allocator ──────────────────────────────────────────────
# The allocator is a bump allocator that tracks a cursor position in WASM
# linear memory. It's immutable — each operation returns an updated allocator.
alloc = Memory.new(wasm)
IO.puts("1️⃣ Allocator created")
IO.puts(" Base offset: #{alloc.base_offset} (avoids clobbering low addresses)")
IO.puts(" Alignment: #{alloc.alignment} bytes")
IO.puts("")
# ─── 2. Writing & Reading Strings ──────────────────────────────────────────
# Most WASM interop involves strings. WASM has no string type — strings are
# just bytes in linear memory, passed as (pointer, length) pairs.
{alloc, ptr, len} = Memory.write_string(alloc, "hello world")
IO.puts("2️⃣ String written")
IO.puts(" Pointer: #{ptr}, Length: #{len} bytes")
{:ok, str} = Memory.read_string(alloc, ptr, len)
IO.puts(" Read back: #{inspect(str)}")
# For C-style APIs that expect null-terminated strings:
{alloc, ptr2, len2} = Memory.write_string(alloc, "null terminated", null_terminated: true)
IO.puts(" Null-terminated: ptr=#{ptr2}, logical_len=#{len2}")
IO.puts("")
# ─── 3. Typed Integer Arrays ───────────────────────────────────────────────
# Pass arrays of numbers to WASM functions like sum_array(ptr, count).
{alloc, arr_ptr, count} = Memory.write_i32_array(alloc, [10, 20, 30, 40, 50])
IO.puts("3️⃣ i32 array written")
IO.puts(" Pointer: #{arr_ptr}, Count: #{count} elements")
{:ok, values} = Memory.read_i32_array(alloc, arr_ptr, count)
IO.puts(" Read back: #{inspect(values)}")
# Also available: write_u32_array, write_i64_array for other integer types
{alloc, _, _} = Memory.write_u32_array(alloc, [1, 2, 3])
{alloc, _, _} = Memory.write_i64_array(alloc, [100_000_000_000, 200_000_000_000])
IO.puts(" Also wrote u32 and i64 arrays")
IO.puts("")
# ─── 4. Float Arrays ───────────────────────────────────────────────────────
# For scientific computing, signal processing, ML inference, etc.
{alloc, f64_ptr, f64_count} = Memory.write_f64_array(alloc, [3.14159, 2.71828, 1.41421])
IO.puts("4️⃣ f64 array written")
IO.puts(" Pointer: #{f64_ptr}, Count: #{f64_count} elements")
{:ok, floats} = Memory.read_f64_array(alloc, f64_ptr, f64_count)
IO.puts(" Read back: #{inspect(floats)}")
# f32 arrays for when you need smaller precision
{alloc, _, _} = Memory.write_f32_array(alloc, [1.0, 2.0, 3.0])
IO.puts(" Also wrote f32 array")
IO.puts("")
# ─── 5. Scalar Values ──────────────────────────────────────────────────────
# Write individual typed values when a WASM function reads from a pointer.
{alloc, i32_ptr} = Memory.write_i32(alloc, 42)
{:ok, 42} = Memory.read_i32(alloc, i32_ptr)
{alloc, f64_ptr2} = Memory.write_f64(alloc, 3.14159)
{:ok, pi} = Memory.read_f64(alloc, f64_ptr2)
IO.puts("5️⃣ Scalar values")
IO.puts(" i32 at #{i32_ptr}: 42")
IO.puts(" f64 at #{f64_ptr2}: #{pi}")
IO.puts("")
# ─── 6. Struct Layouts (Go/Rust/C Interop) ─────────────────────────────────
# Define C-compatible struct layouts for complex data exchange.
# Fields use natural alignment matching C/Rust/TinyGo defaults.
layout =
Memory.define_layout([
{:x, :f64},
{:y, :f64},
{:id, :i32},
{:flags, :u8}
])
IO.puts("6️⃣ Struct layout defined")
IO.puts(
" Fields: #{inspect(Enum.map(layout.fields, fn {name, type} -> "#{name}:#{type}" end))}"
)
IO.puts(" Offsets: #{inspect(layout.offsets)}")
IO.puts(" Total size: #{layout.size} bytes (padded to max alignment)")
# Write a single struct
point = %{x: 1.5, y: 2.5, id: 42, flags: 7}
{alloc, struct_ptr} = Memory.write_struct(alloc, layout, point)
IO.puts(" Wrote struct at offset #{struct_ptr}")
# Read it back
{:ok, read_point} = Memory.read_struct(alloc, layout, struct_ptr)
IO.puts(" Read back: #{inspect(read_point)}")
# Write an array of structs (common for batch processing)
points = [
%{x: 0.0, y: 0.0, id: 1, flags: 0},
%{x: 3.0, y: 4.0, id: 2, flags: 1},
%{x: 6.0, y: 8.0, id: 3, flags: 2}
]
{alloc, arr_struct_ptr, struct_count} = Memory.write_struct_array(alloc, layout, points)
IO.puts(" Wrote #{struct_count} structs starting at offset #{arr_struct_ptr}")
{:ok, read_points} = Memory.read_struct_array(alloc, layout, arr_struct_ptr, struct_count)
IO.puts(" Read back #{length(read_points)} structs")
for p <- read_points do
IO.puts(" Point ##{p.id}: (#{p.x}, #{p.y}) flags=#{p.flags}")
end
IO.puts("")
# ─── 7. Introspection ──────────────────────────────────────────────────────
# Check how much memory you've used.
info = Memory.info(alloc)
IO.puts("7️⃣ Allocator state")
IO.puts(" Allocated: #{info.allocated_bytes} bytes")
IO.puts(" Allocations: #{info.allocation_count}")
IO.puts(" WASM memory: #{info.wasm_memory_bytes} bytes total")
IO.puts("")
# ─── 8. Arena-Style Deallocation ───────────────────────────────────────────
# Instead of tracking individual allocations, reset everything at once.
# This is the recommended pattern for request/response cycles.
IO.puts("8️⃣ Arena-style deallocation")
IO.puts(" Before free: #{Memory.allocated_bytes(alloc)} bytes used")
alloc = Memory.free_all(alloc)
IO.puts(" After free_all: #{Memory.allocated_bytes(alloc)} bytes used")
IO.puts(" All previous pointers are now invalid — memory is reusable")
IO.puts("")
# ─── 9. Scoped Arenas (with_arena) ─────────────────────────────────────────
# Allocate temporary data, use it, then auto-free — like a block scope.
IO.puts("9️⃣ Scoped arena (with_arena)")
IO.puts(" Cursor before: #{alloc.cursor}")
{result, alloc} =
Memory.with_arena(alloc, fn arena ->
# These allocations are temporary
{arena, ptr, len} = Memory.write_string(arena, "temporary data")
{:ok, temp_str} = Memory.read_string(arena, ptr, len)
{arena, _, _} = Memory.write_i32_array(arena, Enum.to_list(1..100))
IO.puts(" Inside arena: #{Memory.allocated_bytes(arena)} bytes allocated")
# Return a result — it survives the arena reset
{temp_str, arena}
end)
IO.puts(" Cursor after: #{alloc.cursor} (reset to before)")
IO.puts(" Result preserved: #{inspect(result)}")
IO.puts("")
# ─── 10. Secure Free ───────────────────────────────────────────────────────
# When memory contained sensitive data (keys, tokens, PII), zero it out.
{alloc, _, _} = Memory.write_string(alloc, "secret-api-key-12345")
IO.puts("🔟 Secure free")
IO.puts(" Wrote sensitive data (#{Memory.allocated_bytes(alloc)} bytes)")
alloc = Memory.free_all_secure(alloc)
IO.puts(" free_all_secure: memory zeroed + cursor reset")
IO.puts("")
# ─── Cleanup ────────────────────────────────────────────────────────────────
Firebird.stop(wasm)
IO.puts("✅ Done! Instance stopped.\n")
IO.puts("""
📚 Key patterns to remember:
# 1. Allocator is immutable — always rebind
{alloc, ptr, len} = Memory.write_string(alloc, "data")
# 2. Strings are (pointer, length) pairs in WASM
{:ok, [result]} = Firebird.call(wasm, :process, [ptr, len])
# 3. Use free_all/1 between batches (arena pattern)
alloc = Memory.free_all(alloc)
# 4. Use with_arena/2 for temporary allocations
{result, alloc} = Memory.with_arena(alloc, fn a -> ... end)
# 5. Use define_layout/1 for Go/Rust/C struct interop
layout = Memory.define_layout([{:x, :f64}, {:y, :f64}])
""")