{
    "componentChunkName": "component---src-pages-blog-mdx-slug-js",
    "path": "/blog/2026-07-05-pico-phone-first-gem/",
    "result": {"data":{"mdx":{"frontmatter":{"title":"I Just Published My First Ruby Gem!","tags":["Ruby","C++","open source","gems","libphonenumber"],"categories":[],"date":"July 05, 2026","image":null,"imageAlt":null},"id":"658785b5-44c0-5ef2-927a-cc321bf54f48","body":"var _excluded = [\"components\"];\nfunction _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }\nfunction _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }\nfunction _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }\n/* @jsxRuntime classic */\n/* @jsx mdx */\n\nvar _frontmatter = {\n  \"title\": \"I Just Published My First Ruby Gem!\",\n  \"date\": \"2026-07-05T00:00:00.000Z\",\n  \"tags\": [\"Ruby\", \"C++\", \"open source\", \"gems\", \"libphonenumber\"],\n  \"categories\": [],\n  \"draft\": false\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n    props = _objectWithoutProperties(_ref, _excluded);\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"I'm happy to report that \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"gem install pico_phone\"), \" works now. That sentence took eighteen months to become true.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"The Problem That Started It\"), mdx(\"p\", null, \"At work, my team validates and parses phone numbers using \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/daddyz/phonelib\"\n  }, \"Phonelib\"), \". It's a solid gem, but it does all of its work in Ruby: regex matching, data table lookups, the works. At scale, that's slow. The actual number-parsing logic it's built on, Google's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/google/libphonenumber\"\n  }, \"libphonenumber\"), \", is a C++ library. Phonelib (and libraries like it) reimplement its behavior in Ruby rather than calling into it directly.\"), mdx(\"p\", null, \"We looked at \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/daddyz/mini_phone\"\n  }, \"mini_phone\"), \", which \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"does\"), \" wrap the C++ library as a native extension. Faster, in principle. But it didn't expose enough of libphonenumber's surface area for what we needed, so it wasn't a real option.\"), mdx(\"p\", null, \"That left an obvious, mildly ridiculous idea: what if I just wrapped libphonenumber myself?\"), mdx(\"p\", null, \"I've liked C and C++ since college. Not in a nostalgic way, more that I still think they're some of the most powerful, useful, multipurpose languages around, and I don't get many excuses to write them anymore. Looking into what it would take to bind a C++ library into Ruby, I found \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/ruby-rice/rice\"\n  }, \"Rice\"), \", a library that makes writing Ruby/C++ extensions look almost reasonable. That was enough to pull me in.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"It Started During a Free Week Back in 2024\"), mdx(\"p\", null, \"My employer runs something called \\\"free week\\\" a few times a year, two weeks where you can work on anything that isn't your assigned work but strikes your fancy. Some genuinely great internal tools have come out of free weeks. Mine, in December 2024, went to pico_phone.\"), mdx(\"p\", null, \"The name is a small joke stacked on a coincidence. \\\"Pico\\\" is smaller than \\\"mini,\\\" so it reads as a nod to mini_phone, the gem we'd ruled out. But Pico also happens to be my employer's mascot, and at the time I half expected this to end up as an internal company gem rather than something I'd publish myself. The name stuck around even after those plans changed.\"), mdx(\"p\", null, \"The early commits tell the story of someone figuring out Rice in real time: exposing a couple of instance variable accessors first, then a \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"valid?\"), \" method, then wrapping the C++ \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"PhoneNumber\"), \" class itself, then discovering the wrap-the-class approach didn't work and redefining it as a proper Ruby class instead. By the end of the two weeks I had a working extension with parsing, validation, and basic formatting.\"), mdx(\"p\", null, \"Rice only solves the C++/Ruby boundary, though. None of the usual gem-development comforts come with it, so a decent chunk of that first week went into wiring up the scaffolding around it. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"Rake::ExtensionTask\"), \" (from rake-compiler) hooks compilation into the Rake task graph, so \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rake spec\"), \" recompiles the extension before RSpec ever loads it, which matters a lot when a C++ change and a spec change land in the same commit and you don't want to debug a spec failure that's actually a stale binary. And \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"bin/console\"), \" recompiles, requires the gem, and drops into a Pry session, so I could poke at a freshly built extension by hand instead of writing a throwaway spec every time I wanted to check what a method actually returned.\"), mdx(\"p\", null, \"That scaffolding is also, in hindsight, most of what made picking the project back up eighteen months later tractable at all. At the time, though, it just felt like a solid foundation, the kind of thing you assume you'll circle back to in a few weeks. Then free week ended, real work resumed, and pico_phone sat untouched for the next year and a half.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"How Rice Actually Binds C++ to Ruby\"), mdx(\"p\", null, \"Rice's pitch is that writing a Ruby extension shouldn't mean writing \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.ruby-lang.org/en/master/extension_rdoc.html\"\n  }, \"Ruby's C extension API\"), \" by hand. Instead of the usual \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"Init_foo\"), \" full of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rb_define_method\"), \" calls and manual \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \" juggling, you get a fluent, C++-flavored builder:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"cpp\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_module\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"PicoPhone\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_singleton_method\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"valid?\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"pico_phone_is_valid_for_default_country\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_singleton_method\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"possible_countries\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"pico_phone_possible_countries_for_string\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// ...\"), \"\\n\\nrb_cPhoneNumber \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_class_under\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"rb_mPicoPhone\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"PhoneNumber\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_method\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"valid?\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"is_parsed_phone_number_valid\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_method\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"national\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"format_parsed_number_national\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"define_method\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"possible_countries\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"parsed_number_possible_countries\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// ...\")))), mdx(\"p\", null, \"Each call to \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"define_method\"), \" takes a plain C++ function pointer. Rice inspects its signature and generates the argument-marshaling code for you: a C++ \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"std::string\"), \" argument becomes a Ruby \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"String\"), \" becomes a \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"std::string\"), \" again on the way in, and a returned \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"bool\"), \" becomes \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"true\"), \"/\", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"false\"), \" on the way out. Rice also ships its own C++-flavored wrapper types, \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"Object\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"String\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"Array\"), \", that behave like their Ruby counterparts but convert implicitly at the boundary. Most of pico_phone's ~40 methods are exactly this: a thin C++ function that calls one or two libphonenumber methods and returns a value Rice already knows how to convert.\"), mdx(\"p\", null, \"Where it gets more hands-on is the \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"PhoneNumber\"), \" class itself. Under the hood, each Ruby \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"PhoneNumber\"), \" instance wraps a heap-allocated libphonenumber protobuf struct, and that struct has its own C++ construction and destruction rules that don't map onto Rice's own class-wrapping machinery cleanly. So that one piece drops down to Ruby's raw C extension API: a custom \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rb_data_type_t\"), \" describing how to free the struct, an \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"alloc\"), \" function that placement-news a \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"PhoneNumber\"), \" into memory Ruby owns, and \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"TypedData_Wrap_Struct\"), \" to hand that pointer to Ruby as an opaque, garbage-collectible object:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"cpp\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"void\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"phone_number_free\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"void\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"*\"), \"data\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"\\n  PhoneNumber \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"*\"), \"phone_number \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token generic-function\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token function\"\n  }, \"static_cast\"), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token generic class-name\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token operator\"\n  }, \"<\"), \"PhoneNumber \", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token operator\"\n  }, \"*\"), mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token operator\"\n  }, \">\"))), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"data\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"\\n  phone_number\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"->\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"~\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"PhoneNumber\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// explicit destructor call, since placement new skips `delete`\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"xfree\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"data\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\"), \"\\n\\nVALUE \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"rb_phone_number_alloc\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"VALUE self\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"{\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"void\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"*\"), \"phone_number_data \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"ALLOC\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"PhoneNumber\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"\\n  PhoneNumber \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"*\"), \"phone_number \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"=\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"new\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"phone_number_data\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"PhoneNumber\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// placement new\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"TypedData_Wrap_Struct\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"self\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token operator\"\n  }, \"&\"), \"phone_number_type\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" phone_number\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"}\")))), mdx(\"p\", null, \"Every instance method then starts by unwrapping that pointer with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"TypedData_Get_Struct\"), \" before it can call into libphonenumber at all. It's more ceremony than the \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"define_method\"), \" calls above, but it's ceremony Rice doesn't have an opinion about: it's really about managing a C++ object's lifetime inside Ruby's garbage collector, not about calling a function.\"), mdx(\"p\", null, \"The two layers coexist in the same file, and that's exactly the seam bug two (below) lived in. Rice's automatic conversion is what makes the \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"define_method\"), \" layer painless, but the moment you drop into the raw C API to do something Rice doesn't cover, that safety net is gone. You're back to knowing, by hand, what a bare \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \" actually means.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Picking It Back Up\"), mdx(\"p\", null, \"I came back to it this past June, mostly on a whim, and ended up going all in over about a week. Same core idea, but this time I actually finished it.\"), mdx(\"p\", null, \"The first pass was bug hunting, and the bugs were the kind that only show up when you're gluing two type systems together.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Bug one:\"), \" a cleanup function that nulls out cached values after a failed parse was writing to the wrong object. Every \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rb_iv_set\"), \" call used the class object instead of \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"self\"), \", meaning every failed parse silently mutated global class state instead of the instance. It \\\"worked\\\" by accident, since memoization never triggered and every method just recomputed from scratch. But it was leaking junk onto the class on every failure.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Bug two\"), \" was the more interesting one:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"cpp\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-cpp\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// before\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"rb_iv_set\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"self\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"@country_code\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" code\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \"  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// code is a raw C int\"), \"\\n\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"// after\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"return\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"rb_iv_set\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"self\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string\"\n  }, \"\\\"@country_code\\\"\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \",\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token function\"\n  }, \"INT2FIX\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), \"code\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\")))), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rb_iv_set\"), \" expects a Ruby-encoded \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \", not a plain C int. The original tests passed anyway, by pure accident: Rice 4 automatically runs \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"LONG2NUM\"), \" on anything a function declares as returning \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \", so the wrong bit pattern got silently corrected on the way out even though the \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"cached\"), \" ivar was garbage. Fixing the ivar broke the return value, which only got fixed once I changed the function's return type from \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \" to \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"Object\"), \", Rice's signal for \\\"this is already a boxed Ruby value, stop converting it.\\\" I now think of this as the central Rice 4 lesson: \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"VALUE\"), \" doesn't mean \\\"Ruby object,\\\" it means \\\"integer I will \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"LONG2NUM\"), \" for you.\\\"\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Bug three\"), \" was a hardcoded 10-digit US formatting pattern that happened to also produce correct results for Brazilian numbers by coincidence, and returned an empty string for everything else. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"GetNationalSignificantNumber\"), \" replaced it and about thirty lines of pattern-matching scaffolding disappeared with it.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Actually Using the C++ Library\"), mdx(\"p\", null, \"Once the bugs were fixed, the fun part started: libphonenumber does a lot more than parse-and-validate, and pico_phone didn't expose any of it yet.\"), mdx(\"p\", null, \"The one I was proudest of is \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"possible_countries\"), \" / \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"valid_countries\"), \". A calling code isn't 1:1 with a country: \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"+1\"), \" covers the US, Canada, and about twenty Caribbean territories; \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"+7\"), \" covers both Russia and Kazakhstan. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"GetRegionCodesForCountryCallingCode\"), \" returns every region sharing a code, and filtering that list with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"IsValidNumberForRegion\"), \" tells you which ones a given number could actually belong to:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"ruby\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ruby\"\n  }, \"PicoPhone\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"possible_countries\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"+15102745656\\\"\")), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# => [\\\"US\\\"]\"), \"\\nPicoPhone\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), \"possible_countries\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"+78005553535\\\"\")), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# => [\\\"RU\\\", \\\"KZ\\\"]\")))), mdx(\"p\", null, \"I also added short number support (\", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"emergency_number?\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"short_number_cost\"), \") via libphonenumber's separate \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"ShortNumberInfo\"), \" class, since regular parsing chokes on \\\"911\\\" and needs its own code path entirely. And \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"possible_with_reason\"), \", which turns a plain boolean into a symbol explaining \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"why\"), \" a number isn't possible (\", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \":too_short\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \":too_long\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \":invalid_country_code\"), \"), which turned out to be much more useful for form validation than I expected.\"), mdx(\"p\", null, \"One method I deliberately didn't build: an as-you-type formatter. libphonenumber supports it, and I could have wrapped it. But real-time formatting on every keystroke means a server round trip per character, and the JS port already does this client-side at zero latency. Not every capability a library exposes is one you should ship.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Getting It Gem-Shaped\"), mdx(\"p\", null, \"Working code and a publishable gem are different things. YARD stubs, for one: since every method is defined in C++ via Rice, tools like Solargraph can't introspect any of it. Typing \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"phone.\"), \" in an editor gave you nothing. The fix is a file that's never actually loaded at runtime, existing only to give IDEs something to read:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"ruby\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# Not loaded at runtime, exists only for YARD and IDE tooling\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"module\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token class-name\"\n  }, \"PicoPhone\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"class\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token class-name\"\n  }, \"PhoneNumber\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# @return [Symbol] :is_possible, :too_short, :too_long, ...\"), \"\\n    \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"def\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token method-definition\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token function\"\n  }, \"possible_with_reason\")), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \";\"), \" \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"end\"), \"\\n  \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"end\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"end\")))), mdx(\"p\", null, \"Then CI, across Ubuntu and macOS, across Ruby 3.1 through 3.4. That run answered a question I'd been quietly worried about: I'd only ever built and tested against libphonenumber 9.0.33 on macOS. Ubuntu's package repos ship 8.x. All eight jobs passed on the first try, which was a relief I probably didn't need to feel this strongly about.\"), mdx(\"p\", null, \"Publishing itself had one hiccup: the default RubyGems API key scope is read-only, so my first \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rake release\"), \" failed with \\\"This API key cannot perform the specified action on this gem.\\\" Fixed by adding push scope to the key, but by then the git tag had already been pushed, so the recovery is \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"gem push\"), \" directly rather than re-running \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"rake release\"), \", which would just fail again on the existing tag.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Falling Down the Native Gem Hole\"), mdx(\"p\", null, \"This is the part that ate most of the week.\"), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"gem install pico_phone\"), \" originally meant: get a C++ compiler, install CMake, then install libphonenumber and its transitive dependencies through Homebrew or apt. Fine for me, a real barrier for anyone trying to add it to a Dockerfile. The fix is what nokogiri and grpc do: ship pre-compiled, platform-specific binaries so most users never compile anything.\"), mdx(\"p\", null, \"I assumed I'd need to vendor three libraries: libphonenumber, protobuf, abseil. Homebrew's formula corrected me:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"ruby\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ruby\"\n  }, \"depends_on \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"abseil\\\"\")), \"\\ndepends_on \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"boost\\\"\")), \"\\ndepends_on \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"icu4c@78\\\"\")), \"\\ndepends_on \", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"protobuf\\\"\"))))), mdx(\"p\", null, \"Five libraries, not three. Boost and ICU are large enough that \\\"vendor the source and build it in CI\\\" stopped being an obvious plan and became a real project. Before committing to it, I ran \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"otool -L\"), \" on the compiled extension to see what it \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"actually\"), \" linked against:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"text\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-text\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"lib/pico_phone/pico_phone.bundle:\\n  /opt/homebrew/opt/libphonenumber/lib/libphonenumber.9.dylib\\n  libruby.3.4.dylib\\n  /usr/lib/libc++.1.dylib\\n  /usr/lib/libSystem.B.dylib\"))), mdx(\"p\", null, \"Good news: only libphonenumber directly. Boost, ICU, protobuf, and abseil are libphonenumber's problem, not mine, directly. Bad news: Homebrew only ships \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"libphonenumber.a\"), \" as a static archive, and protobuf and abseil are dynamic-only. To get a truly self-contained \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \".so\"), \", I had to build protobuf and abseil from source with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-DBUILD_SHARED_LIBS=OFF\"), \", then build libphonenumber against those static archives. Same path nokogiri takes with libxml2, just with a longer dependency chain.\"), mdx(\"p\", null, \"Two platform-specific fights came out of that:\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Boost on macOS.\"), \" As of Boost 1.90, \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"boost::system\"), \" became header-only and \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"libboost_system.a\"), \" stopped existing. Passing it to the linker anyway just gets you \\\"file not found\\\" until you realize the fix is to remove it from the list, not find it somewhere else.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"ICU on Linux.\"), \" The first Linux CI run failed at link time:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"text\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-text\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"relocation R_X86_64_PC32 against symbol `_ZTVN6icu_7413UnicodeStringE'\\ncan not be used when making a shared object; recompile with -fPIC\"))), mdx(\"p\", null, \"Ubuntu's \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"libicu-dev\"), \" static archives simply aren't compiled with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-fPIC\"), \", so they can't go into a shared object. macOS never hits this because Homebrew compiles everything with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-fPIC\"), \" by default. The pragmatic fix was linking ICU dynamically on Linux instead. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"libicu74\"), \" ships as part of the Ubuntu 24.04 base system, so in practice it's invisible to users, even though it means the Linux gem isn't \", mdx(\"em\", {\n    parentName: \"p\"\n  }, \"quite\"), \" as self-contained as the macOS one. Building ICU from source with \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-fPIC\"), \" would close that gap; I decided it wasn't worth it yet.\"), mdx(\"p\", null, \"The payoff: \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"arm64-darwin\"), \", \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"x86_64-linux\"), \", and \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"aarch64-linux\"), \" gems, none of which require anything to compile on install. Only the macOS one is fully self-contained, though, and it shows: at around 13.7 MB, it's on the large side for a Ruby gem, mostly because it bakes in \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"libicudata\"), \", Unicode's character tables, locale data, and collation rules, on its own 8-10 MB. The Linux gems link ICU dynamically instead of statically, so they come in noticeably smaller. I tried stripping debug symbols to shrink the macOS gem and it didn't move the needle at all: the bulk of the size is actual Unicode data, not symbols, so there was barely anything to strip.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"Locking the Door Behind Me\"), mdx(\"p\", null, \"Publishing a gem to the world means someone else's \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"bundle install\"), \" now depends on you not getting compromised. Before calling it done, I went through the release pipeline looking for the obvious ways that could go wrong: pinning GitHub Actions to commit SHAs instead of tags, adding an explicit \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"permissions: contents: read\"), \" block so only the release job can write, enabling branch protection on \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"main\"), \", wiring up Dependabot.\"), mdx(\"p\", null, \"The one I'm most glad I did: RubyGems trusted publishing. Instead of a long-lived API key sitting in a GitHub secret forever, the release workflow now mints a short-lived credential via OIDC at publish time. I didn't just wire it up and assume it worked. I cut a no-op patch release specifically to watch the logs and confirm a fresh key was actually being minted, not the old secret. It was. I left the old key in place as a manual fallback, but the workflow doesn't touch it anymore.\"), mdx(\"p\", null, \"I also added CodeQL and a sanitizer build. \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"-fsanitize=address,undefined\"), \" recompiles the extension with AddressSanitizer and UndefinedBehaviorSanitizer instrumentation baked in, then runs the full RSpec suite (loaded via \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"LD_PRELOAD\"), \" on Linux, \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"DYLD_INSERT_LIBRARIES\"), \" on macOS) against that instrumented build instead of the normal one. ASan catches memory bugs that C++ will happily let slide silently: buffer overflows, use-after-free, double-frees. UBSan catches the C++-specific undefined-behavior category: signed integer overflow, misaligned pointer access, invalid enum values, the kind of thing that \\\"works\\\" on your machine and then corrupts memory on someone else's. Neither turns up in a normal test run; both would crash loudly the moment the instrumented binary hit the bad code path. For a C++ extension parsing untrusted string input from Ruby callers, that felt less like paranoia and more like the actual bar.\"), mdx(\"hr\", null), mdx(\"h2\", null, \"What I'd Tell Past Me\"), mdx(\"p\", null, \"If I'd known in December 2024 what \\\"finishing this\\\" actually meant (Rice's VALUE/Object footgun, five transitive C++ dependencies, a Boost API change I hadn't heard of, OIDC trusted publishing) I might have talked myself out of the free week entirely. I'm glad I didn't know.\"), mdx(\"p\", null, \"The gem is small. It does one thing, and it does it by getting out of the way and letting a C++ library that Google maintains do the actual work. But the eighteen months between \\\"wraps a couple of methods\\\" and \\\"gem install works\\\" is where all of the real learning was: not in writing the C++, but in everything around it that makes a piece of software something other people can actually depend on.\"), mdx(\"p\", null, mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"aarch64-linux\"), \" CI is still missing a dedicated job, and Linux's ICU linking is dynamic when I'd like it to be static. Small things. As of this writing, \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://rubygems.org/gems/pico_phone\"\n  }, \"RubyGems\"), \" shows more than 4,400 downloads, more than I expected for a gem that's been public for a few days. I'm honestly not sure what that number represents, since it counts every \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"bundle install\"), \" and CI run right along with an actual person choosing to depend on this, so I'm treating it as a nice number rather than a real usage signal. For now, though: \", mdx(\"code\", {\n    parentName: \"p\",\n    \"className\": \"language-text\"\n  }, \"gem install pico_phone\"), \".\"));\n}\n;\nMDXContent.isMDXComponent = true;","timeToRead":9,"slug":"2026-07-05-pico-phone-first-gem"},"allMdx":{"edges":[{"next":{"slug":"2013-03-17-silicon-valley-devfest/","frontmatter":{"title":"Silicon Valley DevFest"},"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d"},"previous":null,"node":{"id":"3378703f-653c-58fd-a11e-7adc675fb79e","frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"}},{"next":{"slug":"2015-06-14-in-search-of-a-faster-query","frontmatter":{"title":"In search of a faster query"},"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf"},"previous":{"frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"},"node":{"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d","frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"}},{"next":{"slug":"2015-11-01-diary-of-a-dev-joys-of-building","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"id":"ca6c84a6-3aea-5278-bf26-267d43acb227"},"previous":{"frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"},"node":{"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf","frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"}},{"next":{"slug":"2016-12-24-working-with-d3-based-chart-library/","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478"},"previous":{"frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"},"node":{"id":"ca6c84a6-3aea-5278-bf26-267d43acb227","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"}},{"next":{"slug":"2017-03-15-react-conf-2017/","frontmatter":{"title":"React Conf 2017"},"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa"},"previous":{"frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"},"node":{"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"}},{"next":{"slug":"2017-04-12-the-initial-state-gotcha","frontmatter":{"title":"The Initial State Gotcha"},"id":"82041731-cb81-571d-958c-00f57d99b927"},"previous":{"frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"},"node":{"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa","frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"}},{"next":{"slug":"2017-05-13-blue-socks-at-orange/","frontmatter":{"title":"Blue Socks Spotted at Orange"},"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b"},"previous":{"frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"},"node":{"id":"82041731-cb81-571d-958c-00f57d99b927","frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"}},{"next":{"slug":"once-upon-a-blog/","frontmatter":{"title":"Once upon a Blog"},"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869"},"previous":{"frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"},"node":{"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b","frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"}},{"next":{"slug":"story-of-a-laptop-upgrade","frontmatter":{"title":"Story of a laptop upgrade"},"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a"},"previous":{"frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"},"node":{"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869","frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"}},{"next":{"slug":"the-code-we-write-today/","frontmatter":{"title":"The code we write today"},"id":"08584458-be98-53c2-8cf3-9fa51060e04b"},"previous":{"frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"},"node":{"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a","frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"}},{"next":{"slug":"rails-on-docker/","frontmatter":{"title":"Rails on Docker"},"id":"5e92911a-24f7-562d-8ecd-9a7391960489"},"previous":{"frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"},"node":{"id":"08584458-be98-53c2-8cf3-9fa51060e04b","frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"}},{"next":{"slug":"rails-on-docker-part-two/","frontmatter":{"title":"Rails on Docker Part Two"},"id":"534e725d-71be-5e6f-ace7-75db51185701"},"previous":{"frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"},"node":{"id":"5e92911a-24f7-562d-8ecd-9a7391960489","frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"}},{"next":{"slug":"super-powers-for-active-record-relation","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06"},"previous":{"frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"},"node":{"id":"534e725d-71be-5e6f-ace7-75db51185701","frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"}},{"next":{"slug":"2026-05-30-outsmarting-github-copilot","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1"},"previous":{"frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"},"node":{"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"}},{"next":{"slug":"2026-06-06-first-steps-with-mistral","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15"},"previous":{"frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"},"node":{"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"}},{"next":{"slug":"2026-06-12-building-the-subtitle-tool/","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883"},"previous":{"frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"},"node":{"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"}},{"next":{"slug":"2026-06-19-building-a-voice-assistant/","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f"},"previous":{"frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"},"node":{"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"}},{"next":{"slug":"2026-06-26-knitting-assistant-pattern-ingestion/","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"id":"85983a41-a504-57c0-8385-5f6327fb821c"},"previous":{"frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"},"node":{"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"}},{"next":{"slug":"2026-07-05-pico-phone-first-gem","frontmatter":{"title":"I Just Published My First Ruby Gem!"},"id":"658785b5-44c0-5ef2-927a-cc321bf54f48"},"previous":{"frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"},"node":{"id":"85983a41-a504-57c0-8385-5f6327fb821c","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"}},{"next":null,"previous":{"frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"},"node":{"id":"658785b5-44c0-5ef2-927a-cc321bf54f48","frontmatter":{"title":"I Just Published My First Ruby Gem!"},"slug":"2026-07-05-pico-phone-first-gem"}}]}},"pageContext":{"id":"658785b5-44c0-5ef2-927a-cc321bf54f48","slug":"2026-07-05-pico-phone-first-gem","__params":{"slug":"2026-07-05-pico-phone-first-gem"}}},
    "staticQueryHashes": ["2201243204"]}