Implement a dynamic array (simple vector for marshaling).

While working on marshaling the RenderFont abstraction to Dart FFI and WASM, it became clear that dealing with vectors inside of vectors gets tricky.

We can wrap one single vector into a return struct like:
```
struct EasyToMarshal {
  std::vector<float>* ptrToVector;
  float* dataInVector;
  size_t size;
};
```
This is what we currently do and FFI/WASM is expected to then call in to C++ to delete ptrToVector when it's done with the data.

This gets problematic to do when ptrToVector contains types that internally contain more vectors. The size and memory layout of vector isn't guaranteed across platforms with different STL implementations.

The result from the shaper was a vector of RenderGlyphRuns:
```
struct RenderGlyphRun {
    rcp<RenderFont> font;
    float size;

    std::vector<GlyphID> glyphs;       // [#glyphs]
    std::vector<uint32_t> textOffsets; // [#glyphs]
    std::vector<float> xpos;           // [#glyphs + 1]
};

std::vector<RenderGlyphRun> shapeText(...){}
```

This PR introduces a DynamicArray (totally up for renaming this to something less lame) which makes it really easy to marshal DynamicArrays inside of other DynamicArrays as we know the memory layout is just like rive::Span (a pointer and a size).

Separately, this does make me wonder if we should use uint64_t for size in DynamicArrays so we can guarantee those are always 64 bit (the FFI and WASM tooling doesn't make it easy to marhsal runtime determined sizes).

Diffs=
e62737cf9 include cstring for memcpy
468dd3a2b Add growToFit, assert we didn’t overflow size_t, and in place constructor calls
f0947d32f Add realloc down to actual used size from capacity.
516d0fc12 Move resize to builder and rename to simple array.
091904d22 malloc/realloc version
e574043f7 More windows fixes
c1f5b96ec Fix windows compiler warning.
164911445 Implement a dynamic array (simple vector for marshaling).
diff --git a/.rive_head b/.rive_head
index 73ef988..4f0e506 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-51eb2ee206984a0496ece3f44b5e24ec38827b7c
+e62737cf99c7e9f0d94288425903d581c88dea87
diff --git a/include/rive/render_text.hpp b/include/rive/render_text.hpp
index 202582c..6c5b70b 100644
--- a/include/rive/render_text.hpp
+++ b/include/rive/render_text.hpp
@@ -8,6 +8,7 @@
 #include "rive/math/raw_path.hpp"
 #include "rive/refcnt.hpp"
 #include "rive/span.hpp"
+#include "rive/simple_array.hpp"
 
 namespace rive {
 
@@ -65,13 +66,13 @@
     //
     virtual RawPath getPath(GlyphID) const = 0;
 
-    std::vector<RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text,
-                                          rive::Span<const rive::RenderTextRun> runs) const;
+    rive::SimpleArray<RenderGlyphRun> shapeText(rive::Span<const rive::Unichar> text,
+                                                rive::Span<const rive::RenderTextRun> runs) const;
 
 protected:
     RenderFont(const LineMetrics& lm) : m_LineMetrics(lm) {}
 
-    virtual std::vector<RenderGlyphRun>
+    virtual rive::SimpleArray<RenderGlyphRun>
     onShapeText(rive::Span<const rive::Unichar> text,
                 rive::Span<const rive::RenderTextRun> runs) const = 0;
 
@@ -86,12 +87,19 @@
 };
 
 struct RenderGlyphRun {
+    RenderGlyphRun(size_t glyphCount = 0) :
+        glyphs(glyphCount), textOffsets(glyphCount), xpos(glyphCount + 1) {}
+
+    RenderGlyphRun(rive::SimpleArray<GlyphID> glyphIds,
+                   rive::SimpleArray<uint32_t> offsets,
+                   rive::SimpleArray<float> xs) :
+        glyphs(glyphIds), textOffsets(offsets), xpos(xs) {}
+
     rcp<RenderFont> font;
     float size;
-
-    std::vector<GlyphID> glyphs;       // [#glyphs]
-    std::vector<uint32_t> textOffsets; // [#glyphs]
-    std::vector<float> xpos;           // [#glyphs + 1]
+    rive::SimpleArray<GlyphID> glyphs;       // [#glyphs]
+    rive::SimpleArray<uint32_t> textOffsets; // [#glyphs]
+    rive::SimpleArray<float> xpos;           // [#glyphs + 1]
 };
 
 } // namespace rive
diff --git a/include/rive/simple_array.hpp b/include/rive/simple_array.hpp
new file mode 100644
index 0000000..270a780
--- /dev/null
+++ b/include/rive/simple_array.hpp
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_SIMPLE_ARRAY_HPP_
+#define _RIVE_SIMPLE_ARRAY_HPP_
+
+#include "rive/rive_types.hpp"
+
+#include <initializer_list>
+#include <type_traits>
+#include <cstring>
+
+namespace rive {
+
+template <typename T> class SimpleArrayBuilder;
+
+#ifdef TESTING
+namespace SimpleArrayTesting {
+extern int mallocCount;
+extern int reallocCount;
+void resetCounters();
+} // namespace SimpleArrayTesting
+#endif
+
+/// Lightweight heap array meant to be used when knowing the exact memory layout
+/// of the simple array is necessary, like marshaling the data to Dart/C#/Wasm.
+/// Note that it intentionally doesn't have push/add/resize functionality as
+/// that's reserved for a special case builder that should be to build up the
+/// array. This saves the structure from needing to store extra ptrs and keeps
+/// it optimally sized for marshaling. See SimpleArrayBuilder<T> below for push
+/// functionality.
+template <typename T> class SimpleArray {
+
+public:
+    SimpleArray() : m_ptr(nullptr), m_size(0) {}
+    SimpleArray(size_t size) : m_ptr(static_cast<T*>(malloc(size * sizeof(T)))), m_size(size) {
+        if constexpr (!std::is_pod<T>()) {
+            for (T *element = m_ptr, *end = m_ptr + m_size; element < end; element++) {
+                new (element) T();
+            }
+        }
+
+#ifdef TESTING
+        SimpleArrayTesting::mallocCount++;
+#endif
+    }
+    SimpleArray(const T* ptr, size_t size) : SimpleArray(size) {
+        assert(ptr <= ptr + size);
+        if constexpr (std::is_pod<T>()) {
+            memcpy(m_ptr, ptr, size * sizeof(T));
+        } else {
+            for (T *element = m_ptr, *end = m_ptr + m_size; element < end; element++) {
+                new (element) T(ptr++);
+            }
+        }
+    }
+
+    constexpr SimpleArray(const SimpleArray<T>& other) : SimpleArray(other.m_ptr, other.m_size) {}
+
+    constexpr SimpleArray(SimpleArray<T>&& other) : m_ptr(other.m_ptr), m_size(other.m_size) {
+        other.m_ptr = nullptr;
+        other.m_size = 0;
+    }
+
+    constexpr SimpleArray(SimpleArrayBuilder<T>&& other);
+
+    SimpleArray<T>& operator=(const SimpleArray<T>& other) = delete;
+
+    SimpleArray<T>& operator=(SimpleArray<T>&& other) {
+        this->m_ptr = other.m_ptr;
+        this->m_size = other.m_size;
+        other.m_ptr = nullptr;
+        other.m_size = 0;
+        return *this;
+    }
+
+    template <typename Container>
+    constexpr SimpleArray(Container& c) : SimpleArray(std::data(c), std::size(c)) {}
+    constexpr SimpleArray(std::initializer_list<T> il) :
+        SimpleArray(std::data(il), std::size(il)) {}
+    ~SimpleArray() {
+        if constexpr (!std::is_pod<T>()) {
+            for (T *element = m_ptr, *end = m_ptr + m_size; element < end; element++) {
+                element->~T();
+            }
+        }
+        free(m_ptr);
+    }
+
+    constexpr T& operator[](size_t index) const {
+        assert(index < m_size);
+        return m_ptr[index];
+    }
+
+    constexpr T* data() const { return m_ptr; }
+    constexpr size_t size() const { return m_size; }
+    constexpr bool empty() const { return m_size == 0; }
+
+    constexpr T* begin() const { return m_ptr; }
+    constexpr T* end() const { return m_ptr + m_size; }
+
+    constexpr T& front() const { return (*this)[0]; }
+    constexpr T& back() const { return (*this)[m_size - 1]; }
+
+    // returns byte-size of the entire simple array
+    constexpr size_t size_bytes() const { return m_size * sizeof(T); }
+
+    // Makes rive::SimpleArray std::Container compatible
+    // https://en.cppreference.com/w/cpp/named_req/Container
+    typedef typename std::remove_cv<T>::type value_type;
+    typedef T& reference;
+    typedef T const& const_reference;
+    typedef T* iterator;
+    typedef T const* const_iterator;
+    typedef std::ptrdiff_t difference_type;
+    typedef size_t size_type;
+
+protected:
+    T* m_ptr;
+    size_t m_size;
+};
+
+/// Extension of SimpleArray which can progressively expand as contents are
+/// pushed/added/written to it. Can be released as a simple SimpleArray.
+template <typename T> class SimpleArrayBuilder : public SimpleArray<T> {
+    friend class SimpleArray<T>;
+
+public:
+    SimpleArrayBuilder(size_t reserve) : SimpleArray<T>(reserve) {
+        assert(this->m_ptr <= this->m_ptr + this->m_size);
+        m_write = this->m_ptr;
+    }
+
+    SimpleArrayBuilder() : SimpleArrayBuilder(0) {}
+
+    void add(const T& value) {
+        growToFit();
+        *m_write++ = value;
+    }
+
+    void add(T&& value) {
+        growToFit();
+        *m_write++ = std::move(value);
+    }
+
+    // Allows iterating just the written content.
+    constexpr size_t capacity() const { return this->m_size; }
+    constexpr size_t size() const { return m_write - this->m_ptr; }
+    constexpr T* begin() const { return this->m_ptr; }
+    constexpr T* end() const { return m_write; }
+
+    constexpr T& front() const { return (*this)[0]; }
+    constexpr T& back() const { return *(m_write - 1); }
+
+private:
+    void growToFit() {
+        if (m_write == this->m_ptr + this->m_size) {
+            auto writeOffset = m_write - this->m_ptr;
+            this->resize(std::max((size_t)1, this->m_size * 2));
+            m_write = this->m_ptr + writeOffset;
+        }
+    }
+
+    void resize(size_t size) {
+        if (size == this->m_size) {
+            return;
+        }
+#ifdef TESTING
+        SimpleArrayTesting::reallocCount++;
+#endif
+        if constexpr (!std::is_pod<T>()) {
+            // Call destructor for elements when sizing down.
+            for (T *element = this->m_ptr + size, *end = this->m_ptr + this->m_size; element < end;
+                 element++) {
+                element->~T();
+            }
+        }
+        this->m_ptr = static_cast<T*>(realloc(this->m_ptr, size * sizeof(T)));
+        if constexpr (!std::is_pod<T>()) {
+            // Call constructor for elements when sizing up.
+            for (T *element = this->m_ptr + this->m_size, *end = this->m_ptr + size; element < end;
+                 element++) {
+                new (element) T();
+            }
+        }
+        this->m_size = size;
+    }
+
+    T* m_write;
+};
+
+template <typename T>
+constexpr SimpleArray<T>::SimpleArray(SimpleArrayBuilder<T>&& other) : m_size(other.size()) {
+    // Bring the capacity down to the actual size (this should keep the same
+    // ptr, but that's not guaranteed, so we copy the ptr after the realloc).
+    other.resize(other.size());
+    m_ptr = other.m_ptr;
+    other.m_ptr = nullptr;
+    other.m_size = 0;
+}
+
+} // namespace rive
+
+#endif
diff --git a/skia/renderer/include/renderfont_coretext.hpp b/skia/renderer/include/renderfont_coretext.hpp
index 124249c..9f2b5cb 100644
--- a/skia/renderer/include/renderfont_coretext.hpp
+++ b/skia/renderer/include/renderfont_coretext.hpp
@@ -28,7 +28,7 @@
     std::vector<Coord> getCoords() const override { return m_coords; }
     rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
     rive::RawPath getPath(rive::GlyphID) const override;
-    std::vector<rive::RenderGlyphRun>
+    rive::SimpleArray<rive::RenderGlyphRun>
         onShapeText(rive::Span<const rive::Unichar>,
                     rive::Span<const rive::RenderTextRun>) const override;
 
diff --git a/skia/renderer/include/renderfont_hb.hpp b/skia/renderer/include/renderfont_hb.hpp
index dd8ae3e..0ebac54 100644
--- a/skia/renderer/include/renderfont_hb.hpp
+++ b/skia/renderer/include/renderfont_hb.hpp
@@ -25,7 +25,7 @@
     std::vector<Coord> getCoords() const override;
     rive::rcp<rive::RenderFont> makeAtCoords(rive::Span<const Coord>) const override;
     rive::RawPath getPath(rive::GlyphID) const override;
-    std::vector<rive::RenderGlyphRun>
+    rive::SimpleArray<rive::RenderGlyphRun>
         onShapeText(rive::Span<const rive::Unichar>,
                     rive::Span<const rive::RenderTextRun>) const override;
 
diff --git a/skia/renderer/src/renderfont_coretext.cpp b/skia/renderer/src/renderfont_coretext.cpp
index 21d79f5..89dc0cb 100644
--- a/skia/renderer/src/renderfont_coretext.cpp
+++ b/skia/renderer/src/renderfont_coretext.cpp
@@ -186,16 +186,15 @@
     }
 };
 
-static float
-add_run(rive::RenderGlyphRun* gr, CTRunRef run, uint32_t textStart, float textSize, float startX) {
+static rive::RenderGlyphRun
+add_run(CTRunRef run, uint32_t textStart, float textSize, float& startX) {
     if (auto count = CTRunGetGlyphCount(run)) {
         const float scale = textSize * gInvScale;
 
-        gr->glyphs.resize(count);
-        gr->xpos.resize(count + 1);
-        gr->textOffsets.resize(count);
+        rive::RenderGlyphRun gr(count);
+        gr.size = textSize;
 
-        CTRunGetGlyphs(run, {0, count}, gr->glyphs.data());
+        CTRunGetGlyphs(run, {0, count}, gr.glyphs.data());
 
         AutoSTArray<1024, CFIndex> indices(count);
         AutoSTArray<1024, CGSize> advances(count);
@@ -204,20 +203,20 @@
         CTRunGetStringIndices(run, {0, count}, indices.data());
 
         for (CFIndex i = 0; i < count; ++i) {
-            gr->xpos[i] = startX;
-            gr->textOffsets[i] = textStart + indices[i]; // utf16 offsets, will fix-up later
+            gr.xpos[i] = startX;
+            gr.textOffsets[i] = textStart + indices[i]; // utf16 offsets, will fix-up later
             startX += advances[i].width * scale;
         }
-        gr->xpos[count] = startX;
+        gr.xpos[count] = startX;
+        return gr;
     }
-    return startX;
+    return rive::RenderGlyphRun();
 }
 
-std::vector<rive::RenderGlyphRun>
+rive::SimpleArray<rive::RenderGlyphRun>
 CoreTextRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
                                 rive::Span<const rive::RenderTextRun> truns) const {
-    std::vector<rive::RenderGlyphRun> gruns;
-    gruns.reserve(truns.size());
+    rive::SimpleArrayBuilder<rive::RenderGlyphRun> gruns(truns.size());
 
     uint32_t textIndex = 0;
     float startX = 0;
@@ -249,19 +248,18 @@
         CFIndex runCount = CFArrayGetCount(run_array);
         for (CFIndex i = 0; i < runCount; ++i) {
             CTRunRef runref = (CTRunRef)CFArrayGetValueAtIndex(run_array, i);
-            rive::RenderGlyphRun grun;
-            startX = add_run(&grun, runref, textIndex, tr.size, startX);
+            rive::RenderGlyphRun grun = add_run(runref, textIndex, tr.size, startX);
             if (grun.glyphs.size() > 0) {
                 auto ct = font_from_run(runref);
                 grun.font = convert_to_renderfont(ct, tr.font);
                 grun.size = tr.size;
-                gruns.push_back(std::move(grun));
+                gruns.add(std::move(grun));
             }
         }
         textIndex += tr.unicharCount;
     }
 
-    return gruns;
+    return std::move(gruns);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/skia/renderer/src/renderfont_hb.cpp b/skia/renderer/src/renderfont_hb.cpp
index edf6bde..72b8a5b 100644
--- a/skia/renderer/src/renderfont_hb.cpp
+++ b/skia/renderer/src/renderfont_hb.cpp
@@ -189,12 +189,9 @@
 
     // todo: check for missing glyphs, and perform font-substitution
 
-    rive::RenderGlyphRun gr;
+    rive::RenderGlyphRun gr(glyph_count);
     gr.font = tr.font;
     gr.size = tr.size;
-    gr.glyphs.resize(glyph_count);
-    gr.textOffsets.resize(glyph_count);
-    gr.xpos.resize(glyph_count + 1);
 
     const float scale = tr.size / kStdScale;
     for (unsigned int i = 0; i < glyph_count; i++) {
@@ -213,20 +210,19 @@
 static rive::RenderGlyphRun extract_subset(const rive::RenderGlyphRun& orig,
                                            size_t start,
                                            size_t end) {
-    rive::RenderGlyphRun subset;
+    auto count = end - start;
+    rive::RenderGlyphRun subset(rive::SimpleArray<rive::GlyphID>(&orig.glyphs[start], count),
+                                rive::SimpleArray<uint32_t>(&orig.textOffsets[start], count),
+                                rive::SimpleArray<float>(&orig.xpos[start], count));
     subset.font = std::move(orig.font);
     subset.size = orig.size;
-    subset.glyphs.insert(subset.glyphs.begin(), &orig.glyphs[start], &orig.glyphs[end]);
-    subset.textOffsets.insert(subset.textOffsets.begin(),
-                              &orig.textOffsets[start],
-                              &orig.textOffsets[end]);
-    subset.xpos.insert(subset.xpos.begin(), &orig.xpos[start], &orig.xpos[end + 1]);
     subset.xpos.back() = 0; // since we're now the end of a run
+
     return subset;
 }
 
 static void perform_fallback(rive::rcp<rive::RenderFont> fallbackFont,
-                             std::vector<rive::RenderGlyphRun>* gruns,
+                             rive::SimpleArrayBuilder<rive::RenderGlyphRun>& gruns,
                              const rive::Unichar text[],
                              const rive::RenderGlyphRun& orig) {
     assert(orig.glyphs.size() > 0);
@@ -242,23 +238,21 @@
             auto textStart = orig.textOffsets[startI];
             auto textCount = orig.textOffsets[endI - 1] - textStart + 1;
             auto tr = rive::RenderTextRun{fallbackFont, orig.size, textCount};
-            auto gr = shape_run(&text[textStart], tr, textStart);
-            gruns->push_back(std::move(gr));
+            gruns.add(shape_run(&text[textStart], tr, textStart));
         } else {
             while (endI < count && orig.glyphs[endI] != 0) {
                 ++endI;
             }
-            gruns->push_back(extract_subset(orig, startI, endI));
+            gruns.add(extract_subset(orig, startI, endI));
         }
         startI = endI;
     }
 }
 
-std::vector<rive::RenderGlyphRun>
+rive::SimpleArray<rive::RenderGlyphRun>
 HBRenderFont::onShapeText(rive::Span<const rive::Unichar> text,
                           rive::Span<const rive::RenderTextRun> truns) const {
-    std::vector<rive::RenderGlyphRun> gruns;
-    gruns.reserve(truns.size());
+    rive::SimpleArrayBuilder<rive::RenderGlyphRun> gruns(truns.size());
 
     /////////////////
 
@@ -270,7 +264,7 @@
         auto end = gr.glyphs.end();
         auto iter = std::find(gr.glyphs.begin(), end, 0);
         if (!gFallbackProc || iter == end) {
-            gruns.push_back(std::move(gr));
+            gruns.add(std::move(gr));
         } else {
             // found at least 1 zero in glyphs, so need to perform font-fallback
             size_t index = iter - gr.glyphs.begin();
@@ -278,9 +272,9 @@
             // todo: consider sending more chars if that helps choose a font
             auto fallback = gFallbackProc({&missing, 1});
             if (fallback) {
-                perform_fallback(fallback, &gruns, text.data(), gr);
+                perform_fallback(fallback, gruns, text.data(), gr);
             } else {
-                gruns.push_back(std::move(gr)); // oh well, just keep the missing glyphs
+                gruns.add(std::move(gr)); // oh well, just keep the missing glyphs
             }
         }
     }
@@ -294,7 +288,7 @@
             pos += adv;
         }
     }
-    return gruns;
+    return std::move(gruns);
 }
 
 #endif
\ No newline at end of file
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 45fb4df..4f46023 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -90,7 +90,7 @@
 
 #include "rive/render_text.hpp"
 
-std::vector<RenderGlyphRun>
+rive::SimpleArray<RenderGlyphRun>
 RenderFont::shapeText(rive::Span<const rive::Unichar> text,
                       rive::Span<const rive::RenderTextRun> runs) const {
 #ifdef DEBUG
diff --git a/src/simple_array.cpp b/src/simple_array.cpp
new file mode 100644
index 0000000..ef567d8
--- /dev/null
+++ b/src/simple_array.cpp
@@ -0,0 +1,14 @@
+#ifdef TESTING
+#include "rive/simple_array.hpp"
+namespace rive {
+namespace SimpleArrayTesting {
+int mallocCount = 0;
+int reallocCount = 0;
+void resetCounters() {
+    mallocCount = 0;
+    reallocCount = 0;
+}
+} // namespace SimpleArrayTesting
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/test/simple_array_test.cpp b/test/simple_array_test.cpp
new file mode 100644
index 0000000..e3847a0
--- /dev/null
+++ b/test/simple_array_test.cpp
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/simple_array.hpp>
+#include <catch.hpp>
+#include <rive/render_text.hpp>
+
+using namespace rive;
+
+TEST_CASE("array initializes as expected", "[simple array]") {
+    SimpleArray<int> array;
+    REQUIRE(array.empty());
+    REQUIRE(array.size() == 0);
+    REQUIRE(array.size_bytes() == 0);
+    REQUIRE(array.begin() == array.end());
+}
+
+TEST_CASE("simple array can be created", "[simple array]") {
+    SimpleArray<int> array({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+
+    REQUIRE(!array.empty());
+    REQUIRE(array.size() == 10);
+    REQUIRE(array.size_bytes() == 10 * sizeof(int));
+    REQUIRE(array.begin() + array.size() == array.end());
+
+    int counter = 0;
+    int sum = 0;
+    for (auto s : array) {
+        counter += 1;
+        sum += s;
+    }
+    REQUIRE(counter == 10);
+    REQUIRE(sum == 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9);
+}
+
+TEST_CASE("can iterate simple array", "[simple array]") {
+    const int carray[] = {2, 4, 8, 16};
+    SimpleArray<int> array(carray);
+    int expect = 2;
+    for (auto value : array) {
+        REQUIRE(value == expect);
+        expect *= 2;
+    }
+}
+
+TEST_CASE("can build up a simple array", "[simple array]") {
+    SimpleArrayBuilder<int> builder;
+    builder.add(1);
+    builder.add(2);
+    REQUIRE(builder.size() == 2);
+    REQUIRE(builder.capacity() == 2);
+
+    builder.add(3);
+    REQUIRE(builder.size() == 3);
+    REQUIRE(builder.capacity() == 4);
+
+    int iterationCount = 0;
+    int expect = 1;
+    for (auto value : builder) {
+        REQUIRE(value == expect++);
+        iterationCount++;
+    }
+    // Should only iterate what's been written so far.
+    REQUIRE(iterationCount == 3);
+
+    int reallocCountBeforeMove = SimpleArrayTesting::reallocCount;
+    SimpleArray<int> array = std::move(builder);
+    REQUIRE(array[0] == 1);
+    REQUIRE(array[1] == 2);
+    REQUIRE(array[2] == 3);
+    REQUIRE(array.size() == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == reallocCountBeforeMove + 1);
+}
+
+struct StructA {
+    rive::SimpleArray<uint32_t> numbers;
+};
+
+static SimpleArray<StructA> buildStructs() {
+    SimpleArrayTesting::resetCounters();
+
+    SimpleArray<uint32_t> numbersA({33, 22, 44, 66});
+
+    StructA dataA = {.numbers = std::move(numbersA)};
+    // We moved the data so expect only one alloc and 0 reallocs.
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(dataA.numbers.size() == 4);
+    REQUIRE(numbersA.size() == 0);
+
+    SimpleArray<uint32_t> numbersB({1, 2, 3});
+
+    StructA dataB = {.numbers = std::move(numbersB)};
+    REQUIRE(SimpleArrayTesting::mallocCount == 2);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(dataB.numbers.size() == 3);
+    REQUIRE(numbersB.size() == 0);
+
+    SimpleArray<StructA> structs(2);
+    structs[0] = std::move(dataA);
+    structs[1] = std::move(dataB);
+    // Should've alloc one more time to create the structs object.
+    REQUIRE(SimpleArrayTesting::mallocCount == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(structs.size() == 2);
+    REQUIRE(structs[0].numbers.size() == 4);
+    REQUIRE(structs[1].numbers.size() == 3);
+    return structs;
+}
+
+TEST_CASE("arrays of arrays work", "[simple array]") {
+    auto structs = buildStructs();
+    REQUIRE(SimpleArrayTesting::mallocCount == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(structs.size() == 2);
+    REQUIRE(structs[0].numbers.size() == 4);
+    REQUIRE(structs[1].numbers.size() == 3);
+}
+
+static SimpleArray<StructA> buildStructsWithBuilder() {
+    SimpleArrayTesting::resetCounters();
+    SimpleArrayBuilder<StructA> structs(2);
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    for (int i = 0; i < 3; i++) {
+        SimpleArray<uint32_t> numbers({33, 22, 44, 66});
+        StructA data = {.numbers = std::move(numbers)};
+        structs.add(std::move(data));
+    }
+    REQUIRE(SimpleArrayTesting::mallocCount == 4);
+    // Realloc once because we'd reserved 2 and actually added 3.
+    REQUIRE(SimpleArrayTesting::reallocCount == 1);
+    return std::move(structs);
+}
+
+TEST_CASE("builder arrays of arrays work", "[simple array]") {
+    auto structs = buildStructsWithBuilder();
+    // alloc counters should still be the same
+    REQUIRE(SimpleArrayTesting::mallocCount == 4);
+    // Realloc one more time as we sized down.
+    REQUIRE(SimpleArrayTesting::reallocCount == 2);
+}
\ No newline at end of file
diff --git a/test/span_test.cpp b/test/span_test.cpp
index 605c17e..a4c6ae5 100644
--- a/test/span_test.cpp
+++ b/test/span_test.cpp
@@ -61,3 +61,14 @@
     funca(v);
     funcb(v);
 }
+
+TEST_CASE("can iterate span", "[span]") {
+    const int carray[] = {2, 4, 8, 16};
+
+    auto span = Span(carray);
+    int expect = 2;
+    for (auto value : span) {
+        REQUIRE(value == expect);
+        expect *= 2;
+    }
+}
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
index 94a9472..fc1990c 100644
--- a/viewer/src/viewer_content/text_content.cpp
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -12,7 +12,7 @@
 #include "rive/text/line_breaker.hpp"
 
 using RenderFontTextRuns = std::vector<rive::RenderTextRun>;
-using RenderFontGlyphRuns = std::vector<rive::RenderGlyphRun>;
+using RenderFontGlyphRuns = rive::SimpleArray<rive::RenderGlyphRun>;
 using RenderFontFactory = rive::rcp<rive::RenderFont> (*)(const rive::Span<const uint8_t>);
 
 static bool ws(rive::Unichar c) { return c <= ' '; }
diff --git a/viewer/src/viewer_content/textpath_content.cpp b/viewer/src/viewer_content/textpath_content.cpp
index 4495446..2870bbd 100644
--- a/viewer/src/viewer_content/textpath_content.cpp
+++ b/viewer/src/viewer_content/textpath_content.cpp
@@ -15,11 +15,11 @@
 using namespace rive;
 
 using RenderFontTextRuns = std::vector<RenderTextRun>;
-using RenderFontGlyphRuns = std::vector<RenderGlyphRun>;
+using RenderFontGlyphRuns = rive::SimpleArray<RenderGlyphRun>;
 using RenderFontFactory = rcp<RenderFont> (*)(const Span<const uint8_t>);
 
 template <typename Handler>
-void visit(const std::vector<RenderGlyphRun>& gruns, Vec2D origin, Handler proc) {
+void visit(const Span<RenderGlyphRun>& gruns, Vec2D origin, Handler proc) {
     for (const auto& gr : gruns) {
         for (size_t i = 0; i < gr.glyphs.size(); ++i) {
             auto path = gr.font->getPath(gr.glyphs[i]);
@@ -148,7 +148,7 @@
 
 public:
     TextPathContent() {
-        auto compute_bounds = [](const std::vector<RenderGlyphRun>& gruns) {
+        auto compute_bounds = [](const rive::SimpleArray<RenderGlyphRun>& gruns) {
             AABB bounds = {};
             for (const auto& gr : gruns) {
                 bounds.minY = std::min(bounds.minY, gr.font->lineMetrics().ascent * gr.size);