<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="basisrobotics.tech/feed.xml" rel="self" type="application/atom+xml" /><link href="basisrobotics.tech/" rel="alternate" type="text/html" /><updated>2025-07-06T23:29:52+00:00</updated><id>basisrobotics.tech/feed.xml</id><title type="html">Basis Robotics</title><entry><title type="html">uvtarget - a helper for bundling python in CMake</title><link href="basisrobotics.tech/2025/07/06/uvtarget/" rel="alternate" type="text/html" title="uvtarget - a helper for bundling python in CMake" /><published>2025-07-06T00:00:00+00:00</published><updated>2025-07-06T00:00:00+00:00</updated><id>basisrobotics.tech/2025/07/06/uvtarget</id><content type="html" xml:base="basisrobotics.tech/2025/07/06/uvtarget/"><![CDATA[<p>I’ve released today a tool uvtarget - used to link together multiple python projects into one workspace with a single lockfile, with CMake. <a href="https://github.com/basis-robotics/uvtarget">It’s available here on GitHub</a>.</p>

<p>This took a few weeks of various evenings and weekend afternoons to make, which feels crazy for something that’s not even 500 lines of code. Difficulty was mainly knowing what lines to write and figuring out how uv wanted things to be handled. There were a lot of small cases around making sure the environment CMake was being invoked in didn’t leak into the compilation environment, as well as handling subtle differences between project, workspace and virtual environment.</p>

<h2 id="faq">FAQ</h2>

<p>Q. Why?</p>

<p>A. Basis uses CMake as a lowest common denominator build system. I’m still in the process of integrating Python into basis. I had two problems: (1) I need to be able to pull in arbitrary Python versions into the build environment. (2) I need to easily package up the build environment into some kind of install space, and make sure everything comes along for the ride. Uv is an easy win for this, it just needs some work to integrate with CMake, especially for workflows that end up calling <code class="language-plaintext highlighter-rouge">sudo make install</code>.<sup id="fnref:sudo" role="doc-noteref"><a href="#fn:sudo" class="footnote" rel="footnote">1</a></sup>.</p>

<p>Q. Why create a workspace?</p>

<p>A. In a robotics context, one might have several related python projects that need installed into the same image, that are being developed in parallel. Regardless of the structure we give the projects (one package, multiple packages, etc), if we run tests and simulations on one set of dependencies, we need to also ship the codebase on that same set of dependenies.</p>

<p>Q. Why do we need a meta pyproject?</p>

<p>A. One really can’t have multiple lock files for one environment, so this means one pyproject (1:1 relationship). The alternative is multiple environments, but in my experience this is far more trouble than its worth.</p>

<p>Q. Why autogenerate?</p>

<p>A. For now, basis works on a single CMakeList source tree. Adding additional libraries or units that contain python shouldn’t require doing anything more than adding them to your CMake tree - managing an additional parallel tree of dependencies isn’t great. Along with that, we shouldn’t inherit some other library’s lockfile - we should take the constraints but not the pin. Autogeneration can easily be opted out of, if you prefer to do things the other way around (manually track the libraries going into the venv).</p>

<p>Q. Will this work with ROS?</p>

<p>A. Yes, it should - with some caveats. Instructions are for catkin tools. Under <code class="language-plaintext highlighter-rouge">catkin_make</code> this should actually work “perfectly” but I really don’t recommend it, catkin tools is faster and has better isolation. I don’t know the best way to do this under colcon, but it should also be possible (one might want).</p>

<ul>
  <li>Create a package <code class="language-plaintext highlighter-rouge">pybase</code> or similar, inside that package point at each of your other python packages with <code class="language-plaintext highlighter-rouge">uv_add_pyproject(../other_package)</code> or with whatever path resolution you prefer.</li>
  <li>When you call <code class="language-plaintext highlighter-rouge">uv_initialize</code> make sure you use <code class="language-plaintext highlighter-rouge">UNMANAGED_PYPROJECT_FILE</code></li>
  <li>If other packages depend on having a working python environment (due to needing to use an installed package), make sure they gave catkin depends on <code class="language-plaintext highlighter-rouge">pybase</code>.</li>
  <li>Inside the venv that <code class="language-plaintext highlighter-rouge">uvtarget</code> creates, install a <code class="language-plaintext highlighter-rouge">.pth</code> file pointing at each directory containing ROS python that needs to be imported into the venv. (this can be done in CMake)</li>
  <li>When you source catkin’s <code class="language-plaintext highlighter-rouge">setup.bash</code>, call <code class="language-plaintext highlighter-rouge">export PYTHONPATH="" &amp;&amp; source build/path_to_pybase/.env/bin/activate</code> to get an environment with your packages and not the system packages. YMMV, you may need to recompile things like <code class="language-plaintext highlighter-rouge">moveit</code> or <code class="language-plaintext highlighter-rouge">tf2</code>.</li>
</ul>

<p>Q. What about (compiled) extension modules?</p>

<p>A. I haven’t tried creating extension modules yet - but this should work fine. You may or may not need to add a dependency on <code class="language-plaintext highlighter-rouge">uv_sync</code> to your extension builds, otherwise the sync may fail. Happy to work with you for a solution if you’re interested.</p>

<h2 id="how-does-it-work">How does it work?</h2>

<h3 id="cmake-">cmake …</h3>

<p>Near the root of your project, <code class="language-plaintext highlighter-rouge">uv_initialize(...)</code> is called, setting up your environment’s settings. This sets up some variables in CMake and sets up a hook to run at the end of the CMake generation step. <code class="language-plaintext highlighter-rouge">uv venv --python $version</code> is called to create the virtual environment for development.</p>

<p>Each time you call <code class="language-plaintext highlighter-rouge">uv_add_pyproject</code> or <code class="language-plaintext highlighter-rouge">uv_add_dev_dependency</code>, the arguments are stored off for later.</p>

<p>Finally, when the deferred hook is called, we create a pyproject depending on all of the earlier declared dependencies.</p>

<h3 id="make">make</h3>

<p>The cmake step adds up a build target that calls <code class="language-plaintext highlighter-rouge">uv sync</code> with the correct environment and arguments to sync the environment to the workspace. We call this on every build, uv is so fast that you don’t notice in the cases where nothing has changed. An “optimization” would be to only call <code class="language-plaintext highlighter-rouge">uv sync</code> when a pyproject has changed, but that’s just duplicating the logic uv has internally.</p>

<h3 id="make-install">make install</h3>

<p>We call <code class="language-plaintext highlighter-rouge">uv export</code> with a bunch of flags to generate a requirements.txt for just the dependencies. Then <code class="language-plaintext highlighter-rouge">uv build</code> to build wheels for each workspace member. Finally, we install them in one big swoop with <code class="language-plaintext highlighter-rouge">uv pip install</code>.</p>

<p>There are a bunch of options that could be supported for installation that aren’t currently implemented. Wheel+requirements only export, installing to a non virtual environment, exporting to an installer, etc. The wheel+requirements flag is probably the most useful for alternative workflows.</p>

<h2 id="where-are-you-going-from-here">Where are you going from here?</h2>

<h3 id="with-uvtarget">With uvtarget</h3>

<p>Nothing immediately planned, but happy to accept feature requests and PRs.</p>

<h3 id="with-basis">With basis</h3>

<p>I’m finally on to working on subinterpreter support, allowing for loading arbitrary numbers of Python modules in one process, along side C++ (and someday Rust).</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:sudo" role="doc-endnote">
      <p>I really should fix the directory ownership in basis to not use root owned installs. It’s unneeded, but OTOH it’s nice to at least support it as an option. <a href="#fnref:sudo" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[I’ve released today a tool uvtarget - used to link together multiple python projects into one workspace with a single lockfile, with CMake. It’s available here on GitHub.]]></summary></entry><entry><title type="html">Single process, multiple interpreters, no GIL contention - pre-Python3.12</title><link href="basisrobotics.tech/2025/05/26/python/" rel="alternate" type="text/html" title="Single process, multiple interpreters, no GIL contention - pre-Python3.12" /><published>2025-05-26T00:00:00+00:00</published><updated>2025-05-26T00:00:00+00:00</updated><id>basisrobotics.tech/2025/05/26/python</id><content type="html" xml:base="basisrobotics.tech/2025/05/26/python/"><![CDATA[<h1 id="first-off">First off…</h1>

<p>What’s up with Basis?</p>

<p>Dunno. I’ve spent the past few months working for money rather than for “free”<sup id="fnref:who" role="doc-noteref"><a href="#fn:who" class="footnote" rel="footnote">1</a></sup>. I feel like I’m working more hours for them as I was on Basis, but the distribution is a little bit different. I also had a 35 year old Marshall Amp land in my lap<sup id="fnref:guitar" role="doc-noteref"><a href="#fn:guitar" class="footnote" rel="footnote">2</a></sup>, so I’ve been learning guitar. That being said: I still like working with the framework, and I’ll probably keep using it for my own projects. If nobody uses it, oh well. I need to update the website and license to reflect the current state of things, but there’s no hurry. In any case I’m going to use this as a space to write about some of the robotics/C++/tech stuff I deal with. I don’t think Thomas will mind, anyhow. I have some fun stuff from my day job about how running <code class="language-plaintext highlighter-rouge">stat</code> on a certain directory can cause all networking (really, anything using hard IRQs) to hang for dozens of millseconds.</p>

<p>After driving to South Bay and back several times in a week, I let my mind wander a bit for how I would make Python work in Basis. I came up with a pretty good scheme for the serialization (probably can get away with just reserializing once for other languages), but there’s one big problem - how would I make stuffing multiple interpreters into one process work? Using <a href="https://lwn.net/Articles/820424/">subinterpreters</a> would probably work, but restrict use to 3.12+. While I think a lot of people will want to use the latest and greatest Python, in practice it’s easy to get stuck on an older version due to dependencies. Particularly, I’d like to import rospy message definitions without having to worry about mucking around with <code class="language-plaintext highlighter-rouge">PYTHONPATH</code> to manually add them in. Surely there has to be a way of doing this, right?</p>

<h2 id="a-quick-rundown-on-basiss-architecture">A quick rundown on Basis’s architecture</h2>

<p>Basis loads everything up into the same process space, unless you opt out.<sup id="fnref:processes" role="doc-noteref"><a href="#fn:processes" class="footnote" rel="footnote">3</a></sup> My not so secret belief is that “shared memory” is actually a bit of a scam, and that you can go even faster if you put everything into the same process space and share <code class="language-plaintext highlighter-rouge">shared_ptr</code> around. True zero copy without needing to manage shared memory segments. In my mind, a robot should probably have around 3 or 4 processes running “robotics” code. One for your perception pipeline, one for your planning pipeline, one for your low level controls/safety, and one for everything else dealing with telemetry, visualization, etc. ROS1 can do this with nodelets, though the API for them is poor. <a href="https://docs.ros.org/en/kilted/Tutorials/Intermediate/Composition.html#run-time-composition-using-ros-services-with-a-publisher-and-subscriber">ROS2 does this with “components”</a> - this actually looks like a pretty neat API, good on them. One thing Basis can do that others cannot, as far as I can tell is run without a Coordinator (ROS Master, if you lean that way) if you have only one process in your launch file.</p>

<pre class="mermaid">
flowchart TB
    Coordinator--&gt;p1
    Coordinator--&gt;p2
    
    subgraph p1 [process 1]
        direction TB
        realsense_capture
        perception_scene
    end

    subgraph p2 [process 2]
        direction TB
        ll_controls
    end
</pre>
<p><sup>(side note: I’m noticing now that these diagrams are cut off on Safari - if you have ideas why, lemme know)</sup></p>

<p>This presents a bit of a problem for Python, though. Python (pre 3.12) does not like being run as multiple instances in one process. It just straight out doesn’t work - everything runs on the same runtime, you can’t call <code class="language-plaintext highlighter-rouge">Py_Initialize</code> again to get a second runtime. You can run a separate interpreter using <code class="language-plaintext highlighter-rouge">Py_NewInterpreter</code> but this is mainly for getting a new environment<sup id="fnref:pyenv" role="doc-noteref"><a href="#fn:pyenv" class="footnote" rel="footnote">4</a></sup>, all interpreters still hold the same GIL. For a large python robotics codebase, this would mean horrible contention between interpreters.</p>

<p>I was able to “fix” this, on Python3.8. As far as I know, nobody has ever done this before, but would be very interested in prior art on this. (I found <a href="https://gist.github.com/dutc/eba9b2f7980f400f6287">this gist</a> saying it sorta worked, but broke with multithreading). I found <a href="https://news.ycombinator.com/item?id=32910485">another post on HN</a> from girfan/Gohar Chaudhry that mentions doing the same. I’ve reached out to him to see if he was able to get a fully working solution or not.</p>

<p><strong>Disclaimer: I’m not a Python expert, nor a Linux expert<sup id="fnref:linux" role="doc-noteref"><a href="#fn:linux" class="footnote" rel="footnote">5</a></sup>, and I’m especially not a Python internals expert. I recommend not trying to do anything in this post, except for educational purposes.</strong></p>

<p><strong>Disclaimer 2: This is a somewhat meandering post, if you’re interested in the actual way to do this, go to the <a href="#TLDR">TLDR</a></strong></p>

<h1 id="the-long-road-to-a-solution">The long road to a solution</h1>

<h2 id="getting-it-to-work-sorta">Getting it to work, sorta</h2>

<p>I started off trying just some simple test code in a Unit using <code class="language-plaintext highlighter-rouge">pybind11</code> was essentially.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">pybind11</span><span class="o">::</span><span class="n">scoped_interpreter</span> <span class="n">py</span><span class="p">;</span>
  <span class="n">pybind11</span><span class="o">::</span><span class="n">print</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">);</span>
</code></pre></div></div>

<p>Sure enough, running one copy printed <code class="language-plaintext highlighter-rouge">Hello, World!</code> but loading the Unit twice via a launch file</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="c1"># this launch file loads up the same Unit (shared object) twice, with two different names and sets of arguments</span>
<span class="na">units</span><span class="pi">:</span>
  <span class="na">py_a</span><span class="pi">:</span>
    <span class="na">unit</span><span class="pi">:</span> <span class="s">pybind_test</span>
    <span class="na">args</span><span class="pi">:</span>
      <span class="na">pub</span><span class="pi">:</span> <span class="s">True</span>
  <span class="na">py_b</span><span class="pi">:</span>
    <span class="na">unit</span><span class="pi">:</span> <span class="s">pybind_test</span>
    <span class="na">args</span><span class="pi">:</span>
      <span class="na">pub</span><span class="pi">:</span> <span class="s">False</span>
</code></pre></div></div>
<p>got me this error</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">12255.044380416</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="n">thread</span> <span class="k">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="n">terminate</span> <span class="n">called</span> <span class="n">after</span> <span class="n">throwing</span> <span class="n">an</span> <span class="n">instance</span> <span class="n">of</span> <span class="s">'std::runtime_error'</span>
  <span class="n">what</span><span class="p">():</span>  <span class="n">The</span> <span class="n">interpreter</span> <span class="ow">is</span> <span class="n">already</span> <span class="n">running</span>
</code></pre></div></div>

<p>This is entirely expected, and documented in both pybind and Python.</p>

<h3 id="the-initial-fix">The initial “fix”</h3>

<p>Basis Units are loaded via <code class="language-plaintext highlighter-rouge">dlopen</code>, essentially as plugins. If you’ve never seen a call to <code class="language-plaintext highlighter-rouge">dlopen</code> before, it looks like this.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// For now - need to use RTLD_GLOBAL to allow different inproc transports to communicate</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">handle</span> <span class="o">=</span> <span class="n">dlopen</span><span class="p">(</span><span class="n">path</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">RTLD_NOW</span> <span class="o">|</span> <span class="n">RTLD_GLOBAL</span> <span class="p">);</span>
</code></pre></div></div>
<p>This call takes a path to a shared object and loads it into a handle that one can pull symbols from.</p>
<details>
  <summary><strong>If you’re interested in <em>how</em> dlopen is used and how to load an arbitrary function for a shared object, expand this</strong></summary>
  <p></p>

  <p>Each Unit declares a function that will be used as the interface to the plugin loader - declaring as <code class="language-plaintext highlighter-rouge">extern "C"</code> allows for referencing them by name without having to know how C++ types are mangled.</p>
  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// create_unit.h</span>
<span class="k">extern</span> <span class="s">"C"</span> <span class="p">{</span>
<span class="cm">/**
 * Forward declaration of CreateUnit - declared once in each unit library to provide an easy interface to create the
 * contained unit without prior type knowledge. Basically - the entrypoint into a unit "plugin"
 * ...more docs...
 */</span>
<span class="n">basis</span><span class="o">::</span><span class="n">Unit</span> <span class="o">*</span><span class="n">CreateUnit</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string_view</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">unit_name_override</span><span class="p">,</span>
                        <span class="k">const</span> <span class="n">basis</span><span class="o">::</span><span class="n">arguments</span><span class="o">::</span><span class="n">CommandLineTypes</span> <span class="o">&amp;</span><span class="n">command_line</span><span class="p">,</span>
                        <span class="n">basis</span><span class="o">::</span><span class="n">CreateUnitLoggerInterface</span> <span class="n">error_logger</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// create_unit.cpp</span>

<span class="c1">// For now - need to use RTLD_GLOBAL to allow different inproc transports to communicate</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">handle</span> <span class="o">=</span> <span class="n">dlopen</span><span class="p">(</span><span class="n">path</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">RTLD_NOW</span> <span class="o">|</span> <span class="n">RTLD_GLOBAL</span> <span class="p">);</span>

<span class="c1">// ...</span>

<span class="k">using</span> <span class="n">CreateUnitCallback</span> <span class="o">=</span> <span class="k">decltype</span><span class="p">(</span><span class="n">CreateUnit</span><span class="p">)</span> <span class="o">*</span><span class="p">;</span>
<span class="k">auto</span> <span class="n">load_unit</span> <span class="o">=</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">CreateUnitCallback</span><span class="o">&gt;</span><span class="p">(</span><span class="n">dlsym</span><span class="p">(</span><span class="n">handle</span><span class="p">,</span> <span class="s">"CreateUnit"</span><span class="p">));</span>

<span class="c1">// ...</span>

<span class="n">Unit</span><span class="o">*</span> <span class="n">unit</span> <span class="o">=</span> <span class="n">load_unit</span><span class="p">(...);</span>
</code></pre></div>  </div>

  <p>This is pretty straightforward code, for dealing with linux internals. We take a path to a shared object, load it up, grab a named symbol from it, cast it to correct type, and call it. Inside, it essentially calls <code class="language-plaintext highlighter-rouge">return new MyUnitType()</code>, along with doing some other work to set up context for the unit, such as name, environment, args, etc.</p>

</details>

<p></p>

<p>Even after trying <code class="language-plaintext highlighter-rouge">RTLD_LOCAL</code> and <code class="language-plaintext highlighter-rouge">RTLD_DEEPBIND</code> to try and get things a little more self contained, we still got a crash. Turns out that <code class="language-plaintext highlighter-rouge">dlopen</code> will give you the same handle if called twice on the same path. Makes sense, somewhat. I could get around this by making a <code class="language-plaintext highlighter-rouge">my_unit.so</code> and a <code class="language-plaintext highlighter-rouge">my_unit_2.so</code>, and loading them separately with the proper flags to not share symbols…but it’s not enough. They are still using the same underlying <code class="language-plaintext highlighter-rouge">python3.8.so</code>. I could do the same trick to the Python lib, but now I have to worry about any underlying libraries, etc, etc. Not tenable, especially given I won’t have control over what pip installed libs will load. Surely there’s a better way, right?</p>

<p>There is, and it’s called <a href="https://man7.org/linux/man-pages/man3/dlmopen.3.html"><code class="language-plaintext highlighter-rouge">dlmopen</code></a>. I’d never heard of it before reading about it tonight (I’m not sure if I found it on Stack Overflow first or with a friendly AI bot). Some history on why it exists is <a href="https://sourceware.org/glibc/wiki/LinkerNamespaces">here</a>. As mentioned in the article, and as we’ll see later, it’s good for isolating libraries, but can be a somewhat leaky abstraction.</p>

<p>The usage is pretty simple for this case - just pass in <code class="language-plaintext highlighter-rouge">LM_ID_NEWLM</code> as the first argument (to declare a new symbol namespace), and now this library doesn’t have to share any symbols with the rest of the process.<sup id="fnref:linkmap" role="doc-noteref"><a href="#fn:linkmap" class="footnote" rel="footnote">6</a></sup></p>

<p>Using it like this</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="o">*</span><span class="n">handle</span> <span class="o">=</span> <span class="n">dlmopen</span><span class="p">(</span><span class="n">LM_ID_NEWLM</span><span class="p">,</span> <span class="n">path</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">RTLD_NOW</span> <span class="p">);</span>
</code></pre></div></div>
<p>we get a somewhat successful looking log:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">loading</span> <span class="n">unit</span> <span class="s">"/opt/basis/unit/pybind_test.unit.so"</span>
<span class="p">[</span><span class="mi">1970</span><span class="o">-</span><span class="mo">01</span><span class="o">-</span><span class="mo">01</span> <span class="mo">03</span><span class="o">:</span><span class="mi">50</span><span class="o">:</span><span class="mf">51.588</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_b</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="p">[</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">cpp</span><span class="o">:</span><span class="mi">10</span><span class="p">]</span> <span class="n">About</span> <span class="n">to</span> <span class="n">call</span> <span class="n">python</span> <span class="n">interpreter</span> <span class="n">function</span>
<span class="n">Hello</span><span class="p">,</span> <span class="n">World</span><span class="o">!</span>
<span class="p">[</span><span class="mf">13094.921738966</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="n">loading</span> <span class="n">unit</span> <span class="s">"/opt/basis/unit/pybind_test.unit.so"</span>
<span class="p">[</span><span class="mi">1970</span><span class="o">-</span><span class="mo">01</span><span class="o">-</span><span class="mo">01</span> <span class="mo">03</span><span class="o">:</span><span class="mi">50</span><span class="o">:</span><span class="mf">51.608</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_a</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="p">[</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">cpp</span><span class="o">:</span><span class="mi">10</span><span class="p">]</span> <span class="n">About</span> <span class="n">to</span> <span class="n">call</span> <span class="n">python</span> <span class="n">interpreter</span> <span class="n">function</span>
<span class="n">Hello</span><span class="p">,</span> <span class="n">World</span><span class="o">!</span>
<span class="p">[</span><span class="mf">13094.942370882</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
</code></pre></div></div>
<p>Awesome! It works! We’re done now, right?</p>

<p>No - remember that comment in the original code? “<code class="language-plaintext highlighter-rouge">need to use RTLD_GLOBAL to allow different inproc transports to communicate</code>”. While I generally like to avoid it, Basis uses <code class="language-plaintext highlighter-rouge">static</code> in one important place - to make inproc type safe communication channels between Units.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">InprocTransport</span> <span class="p">{</span>
  <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span> <span class="n">InprocConnectorBase</span> <span class="o">*</span><span class="n">GetConnector</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="n">GetConnectorInternal</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">();</span> <span class="p">}</span>

<span class="nl">private:</span>
  <span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span> <span class="n">InprocConnector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="o">*</span><span class="n">GetConnectorInternal</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// TODO: this static somewhat breaks the nice patterns around being explicit about how objects are initialized</span>
    <span class="k">static</span> <span class="n">InprocConnector</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span> <span class="n">connector</span><span class="p">;</span> <span class="c1">// this is basically a wrapper around a list of typed subscribers </span>
    <span class="k">return</span> <span class="o">&amp;</span><span class="n">connector</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This TODO is coming back to bite me a little bit. I could change the Unit API to find some clever way to inject these into newly created units, but it’s hurting my head trying to keep it somewhat type safe and ODR correct. The templating will make this a real pain, I think it restricts me from making one <code class="language-plaintext highlighter-rouge">InprocTransport</code> and injecting it in. I think I’d have to iterate each message type the Unit can use, keep some sort of map at the launcher level, do more magic with <code class="language-plaintext highlighter-rouge">dlsym</code> etc, etc. It’s almost certainly possible, but might be a mess. Even if I fixed that, we use <code class="language-plaintext highlighter-rouge">static</code> variables in other places, like logging - look at the 1970 date in the log above - by doing this we broke the global log formatter. I’m not willing to give up all uses of <code class="language-plaintext highlighter-rouge">static</code>, it’s just too convenient.<sup id="fnref:static" role="doc-noteref"><a href="#fn:static" class="footnote" rel="footnote">7</a></sup></p>

<p>The good news: instead of loading the whole unit in a separate namespace, we can load Python into a new namespace.</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Open the shared object</span>
<span class="kt">void</span><span class="o">*</span> <span class="n">pyhandle</span> <span class="o">=</span> <span class="n">dlmopen</span><span class="p">(</span><span class="n">LM_ID_NEWLM</span><span class="p">,</span> <span class="s">"libpython3.8.so"</span><span class="p">,</span> <span class="n">RTLD_NOW</span> <span class="o">|</span> <span class="n">RTLD_LOCAL</span> <span class="o">|</span> <span class="n">RTLD_DEEPBIND</span><span class="p">);</span>

<span class="c1">// For each symbol we want to use, put a shim in between our call and the python lib</span>
<span class="cp">#define SHIM_PY(f) auto f = reinterpret_cast&lt;decltype(::f)*&gt;(dlsym(pyhandle, #f)); if (!f) { \
  std::cerr &lt;&lt; "dlerror: " &lt;&lt; dlerror() &lt;&lt; std::endl; \
} while(false)
</span>
<span class="c1">// ie</span>
<span class="c1">// auto Py_IsInitialized = reinterpret_cast&lt;decltype(::Py_IsInitialized)*&gt;(dlsym(pyhandle, "Py_IsInitialized"));</span>
<span class="n">SHIM_PY</span><span class="p">(</span><span class="n">Py_IsInitialized</span><span class="p">);</span>
<span class="n">SHIM_PY</span><span class="p">(</span><span class="n">Py_InitializeEx</span><span class="p">);</span>
<span class="n">SHIM_PY</span><span class="p">(</span><span class="n">Py_Finalize</span><span class="p">);</span>
<span class="n">SHIM_PY</span><span class="p">(</span><span class="n">PyRun_SimpleStringFlags</span><span class="p">);</span> <span class="c1">// beware - PyRun_SimpleString won't work here as it's a define</span>

<span class="c1">// Now we can pretend we're calling the global python API but we're actually using our shim</span>
<span class="n">Py_InitializeEx</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">Py_IsInitialized</span><span class="p">());</span>
<span class="n">PyRun_SimpleStringFlags</span><span class="p">(</span><span class="s">"print('Hello from Python!')"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
</code></pre></div></div>
<p>Doing this we get the expected output:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basis</span><span class="err">@</span><span class="mi">395</span><span class="n">dd188e6a2</span><span class="o">:/</span><span class="n">basis</span><span class="o">/</span><span class="n">build</span><span class="err">$</span> <span class="n">basis</span> <span class="n">launch</span> <span class="p">..</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="o">/</span><span class="n">launch</span><span class="o">/</span><span class="n">two</span><span class="p">.</span><span class="n">launch</span><span class="p">.</span><span class="n">yaml</span> 
<span class="p">[</span><span class="mf">20410.047686838</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Running</span> <span class="n">process</span> <span class="s">"/"</span> <span class="n">with</span> <span class="mi">2</span> <span class="n">units</span>
  <span class="o">/</span><span class="n">py_b</span><span class="o">:</span> <span class="n">pybind_test</span> <span class="o">--</span><span class="n">pub</span> <span class="n">False</span>
  <span class="o">/</span><span class="n">py_a</span><span class="o">:</span> <span class="n">pybind_test</span> <span class="o">--</span><span class="n">pub</span> <span class="n">True</span>
<span class="n">Hello</span> <span class="n">from</span> <span class="n">Python</span><span class="o">!</span>
<span class="p">[</span><span class="mf">20410.060431713</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="n">Hello</span> <span class="n">from</span> <span class="n">Python</span><span class="o">!</span>
<span class="p">[</span><span class="mf">20410.070156171</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
</code></pre></div></div>

<p>At this point, I thought to myself: “Great! it should be smooth sailing from now on - I just have to find a way to wrap this into a nice library. I can either use some macro magic to make pybind compatible with this usage of Python, or I can put pybind and Python in a library, shim it, and then I’ll finish up the blog post.”</p>

<p>This was very wrong, and wasted enough time that I could have just written the rest of the python bindings that I actually needed for this in the time it took. The dangers of not having to worry about if the thing you’re writing is useful, I guess.</p>

<p>This approach broke the moment I tried to run Python code in a different thread, which is super important for use in Basis. It’s fairly complex, but the main issue is that <a href="https://man7.org/linux/man-pages/man3/pthread_getspecific.3p.html"><code class="language-plaintext highlighter-rouge">pthread_getspecific</code></a> and friends (used to dynamically create thread local variables) was returning different values when called directly or via a Python API call via <code class="language-plaintext highlighter-rouge">dlmopen/dlsym</code>. This happened because I ended up with two <code class="language-plaintext highlighter-rouge">glibc</code>. I can’t pin my finger on why this was bad, but I believe it had something to do with the glibc creating new threads not being the same one responsible for managing the thread locals used internally by Python.<sup id="fnref:smell" role="doc-noteref"><a href="#fn:smell" class="footnote" rel="footnote">8</a></sup></p>

<p>It took me a while to even understand what was broken. Asking an LLM was mainly met with what the LLM might approximate as horror (as much as an LLM can feel anyhow) about what I was doing, rather than actual helpful advice for tracking down a fix. Finally after a bunch of research, I found a fix: we can inject the pthread portions of glibc from the main process space into our namespaced python.</p>

<h3 id="the-real-fix">The “real” “fix”</h3>

<ol>
  <li>Load <code class="language-plaintext highlighter-rouge">shim_python.so</code>, which links against <code class="language-plaintext highlighter-rouge">libpython3.8.so</code><sup id="fnref:sos" role="doc-noteref"><a href="#fn:sos" class="footnote" rel="footnote">9</a></sup></li>
  <li>Inject a struct containing the host implementation of the functions I wanted to override</li>
  <li>Add implementations of each function forwarding to the host version</li>
</ol>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Header</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="c1">// Needed for TLS to work properly</span>
    <span class="kt">void</span><span class="o">*</span> <span class="p">(</span><span class="o">*</span><span class="n">pthread_getspecific</span><span class="p">)(</span><span class="n">pthread_key_t</span><span class="p">);</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">pthread_setspecific</span><span class="p">)(</span><span class="n">pthread_key_t</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span><span class="o">*</span><span class="p">);</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">pthread_key_create</span><span class="p">)(</span><span class="n">pthread_key_t</span><span class="o">*</span><span class="p">,</span> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="kt">void</span><span class="o">*</span><span class="p">));</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">pthread_key_delete</span><span class="p">)(</span><span class="n">pthread_key_t</span><span class="p">);</span>
    <span class="c1">// Needed for threading.Thread to work properlyßå</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">pthread_create</span><span class="p">)</span> <span class="p">(</span><span class="n">pthread_t</span> <span class="o">*</span><span class="kr">__restrict</span> <span class="n">__newthread</span><span class="p">,</span>
			   <span class="k">const</span> <span class="n">pthread_attr_t</span> <span class="o">*</span><span class="kr">__restrict</span> <span class="n">__attr</span><span class="p">,</span>
			   <span class="kt">void</span> <span class="o">*</span><span class="p">(</span><span class="o">*</span><span class="n">__start_routine</span><span class="p">)</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">),</span>
			   <span class="kt">void</span> <span class="o">*</span><span class="kr">__restrict</span> <span class="n">__arg</span><span class="p">)</span> <span class="n">__THROWNL</span> <span class="n">__nonnull</span> <span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">));</span>
    <span class="c1">// Note: we should probably also add pthread destruction here as well - YOLO</span>
<span class="p">}</span> <span class="n">pthread_shim_table_t</span><span class="p">;</span>

<span class="n">pthread_shim_table_t</span><span class="o">*</span> <span class="n">shim_pthread_table</span><span class="p">;</span>

<span class="c1">// Impl</span>
<span class="kt">void</span> <span class="o">*</span><span class="nf">pthread_getspecific</span><span class="p">(</span><span class="n">pthread_key_t</span> <span class="n">key</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="n">shim_pthread_table</span><span class="o">-&gt;</span><span class="n">pthread_getspecific</span><span class="p">(</span><span class="n">key</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now when Python uses any thread machinery, it uses the pthread implementation from the main thread. It’s even safe to initialize two Python instances on the main thread this way - they each have their own separate thread local storage for their state.</p>

<p>Running <code class="language-plaintext highlighter-rouge">basis launch ../unit/pybind_test/launch/two.launch.yaml</code> again we get:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">197792.817437253</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Running</span> <span class="n">process</span> <span class="s">"/"</span> <span class="n">with</span> <span class="mi">2</span> <span class="n">units</span>
  <span class="o">/</span><span class="n">py_b</span><span class="o">:</span> <span class="n">pybind_test</span> <span class="o">--</span><span class="n">pub</span> <span class="n">False</span>
  <span class="o">/</span><span class="n">py_a</span><span class="o">:</span> <span class="n">pybind_test</span> <span class="o">--</span><span class="n">pub</span> <span class="n">True</span>
<span class="n">Running</span> <span class="n">on</span> <span class="n">main</span> <span class="kr">thread</span>
<span class="n">Kicking</span> <span class="n">off</span> <span class="n">code</span> <span class="n">on</span> <span class="n">a</span> <span class="kr">thread</span><span class="o">!</span>
<span class="p">[</span><span class="mf">197792.968906169</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="n">Running</span> <span class="n">on</span> <span class="n">main</span> <span class="kr">thread</span>
<span class="n">Kicking</span> <span class="n">off</span> <span class="n">code</span> <span class="n">on</span> <span class="n">a</span> <span class="kr">thread</span><span class="o">!</span>
<span class="p">[</span><span class="mf">197793.060450753</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="n">This</span> <span class="n">is</span> <span class="n">a</span> <span class="n">callback</span> <span class="n">from</span> <span class="n">a</span> <span class="n">Python</span> <span class="kr">thread</span><span class="p">.</span>
<span class="n">InprocTestTrigger</span><span class="p">()</span>
<span class="n">This</span> <span class="n">is</span> <span class="n">a</span> <span class="n">callback</span> <span class="n">from</span> <span class="n">a</span> <span class="n">Python</span> <span class="kr">thread</span><span class="p">.</span>
<span class="n">InprocTestTrigger</span><span class="p">()</span>
<span class="n">Sending</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">to</span> <span class="n">another</span> <span class="n">unit</span><span class="p">()</span>
<span class="p">[</span><span class="mf">197794.067803003</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_b</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Got</span> <span class="n">an</span> <span class="n">inproc</span> <span class="n">trigger</span>
<span class="n">Got</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">from</span> <span class="n">another</span> <span class="n">unit</span>
<span class="p">[</span><span class="mf">197794.068057087</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_b</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Current</span> <span class="n">t_count</span> <span class="mi">1</span>
<span class="p">[</span><span class="mf">197794.069997545</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_a</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Got</span> <span class="n">an</span> <span class="n">inproc</span> <span class="n">trigger</span>
<span class="n">Got</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">from</span> <span class="n">another</span> <span class="n">unit</span>
<span class="p">[</span><span class="mf">197794.070058420</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_a</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Current</span> <span class="n">t_count</span> <span class="mi">1</span>
<span class="n">InprocTestTrigger</span><span class="p">()</span>
<span class="n">This</span> <span class="n">is</span> <span class="n">a</span> <span class="n">callback</span> <span class="n">from</span> <span class="n">a</span> <span class="n">Python</span> <span class="kr">thread</span><span class="p">.</span>
<span class="n">InprocTestTrigger</span><span class="p">()</span>
<span class="n">Sending</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">to</span> <span class="n">another</span> <span class="n">unit</span><span class="p">()</span>
<span class="n">Got</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">from</span> <span class="n">another</span> <span class="n">unit</span>
<span class="p">[</span><span class="mf">197795.066806504</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_a</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Got</span> <span class="n">an</span> <span class="n">inproc</span> <span class="n">trigger</span>
<span class="n">This</span> <span class="n">is</span> <span class="n">a</span> <span class="n">callback</span> <span class="n">from</span> <span class="n">a</span> <span class="n">Python</span> <span class="kr">thread</span><span class="p">.</span>
<span class="p">[</span><span class="mf">197795.066806504</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_b</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Got</span> <span class="n">an</span> <span class="n">inproc</span> <span class="n">trigger</span>
<span class="n">Got</span> <span class="n">a</span> <span class="n">trigger</span> <span class="n">from</span> <span class="n">another</span> <span class="n">unit</span>
<span class="p">[</span><span class="mf">197795.066937087</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_a</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Current</span> <span class="n">t_count</span> <span class="mi">1</span>
<span class="p">[</span><span class="mf">197795.066944337</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">py_b</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Current</span> <span class="n">t_count</span> <span class="mi">2</span>
</code></pre></div></div>
<p>I won’t go into the details, but this is running Python code in both separate C++ threads as well as threads kicked off from Python, with confirmation that two C++ threads are holding two separate GILs.</p>

<h3 id="hark-a-bug-report">Hark, a bug report</h3>

<p>As I was writing this post, I came upon <a href="https://sourceware.org/bugzilla/show_bug.cgi?id=24776">this post to Sourceware claiming a bug against glibc</a>. I’m not quite sure the context, but they run into other issues around multiple copies of glibc, and suggest <a href="https://sourceware.org/bugzilla/attachment.cgi?id=15997&amp;action=diff">an even better workaround</a>. Instead of injecting from the main namespace, they reach into it from the side namespace via <code class="language-plaintext highlighter-rouge">dlmopen (LM_ID_BASE, LIBC_SO, RTLD_LAZY);</code>. I like it. I’m not going to rewrite what I have to match it, but I like it.</p>

<h2 id="cleaning-up-loose-ends">Cleaning up loose ends</h2>

<h3 id="importing-numpy---locales-are-fun">Importing numpy - locales are “fun”</h3>

<p>After getting this working, I tried to <code class="language-plaintext highlighter-rouge">import numpy</code>, and got crashes around string parsing. This was very odd, and after some more tinkering, turned out to be locale problems.<a href="https://man7.org/linux/man-pages/man3/uselocale.3.html"> Did you know that when you call <code class="language-plaintext highlighter-rouge">islower</code> in C(++), the results depend on the locale for your thread?</a> That’s right, more thread local shenanagins. It <em>somewhat</em> makes sense in a C fashion that you might want to easily isolate changes to locales, and do it via a thread locale. The other choice is globally setting the locale via <code class="language-plaintext highlighter-rouge">setlocale</code>. Neither option feels great to me. <a href="https://wiki.gentoo.org/wiki/Musl_usage_guide#Locales">Notably, musl chose not to really implement them - I wonder how widely they are intentionally used nowadays.</a></p>

<details>
  <summary><strong>I fixed this by implementing my own non-TLS using versions of them, expand if you’re curious. I probably should have just copied from musl.</strong></summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="kt">int32_t</span> <span class="o">**</span><span class="nf">__ctype_tolower_loc</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">static</span> <span class="kt">int32_t</span> <span class="n">table</span><span class="p">[</span><span class="mi">384</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
  <span class="k">static</span> <span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ptr</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">384</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="n">i</span> <span class="o">&gt;=</span> <span class="sc">'A'</span> <span class="o">||</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="sc">'Z'</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">table</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span> <span class="o">|</span> <span class="mi">32</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="n">table</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">ptr</span> <span class="o">=</span> <span class="n">table</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="o">&amp;</span><span class="n">ptr</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">const</span> <span class="kt">int32_t</span> <span class="o">**</span><span class="n">__ctype_toupper_loc</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">static</span> <span class="kt">int32_t</span> <span class="n">table</span><span class="p">[</span><span class="mi">384</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
  <span class="k">static</span> <span class="k">const</span> <span class="kt">int32_t</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ptr</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">384</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="n">i</span> <span class="o">&gt;=</span> <span class="sc">'A'</span> <span class="o">||</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="sc">'Z'</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">table</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span> <span class="o">-</span> <span class="mi">32</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="p">{</span>
            <span class="n">table</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">ptr</span> <span class="o">=</span> <span class="n">table</span><span class="p">;</span>
  <span class="p">}</span>  

  <span class="k">return</span> <span class="o">&amp;</span><span class="n">ptr</span><span class="p">;</span>
<span class="p">}</span>


<span class="c1">// This was borrowed from stack overflow post, sorry</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">b_loc_table</span><span class="p">[</span><span class="mi">384</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x2003</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x2002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x2002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x2002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x2002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="mh">0x0002</span><span class="p">,</span> <span class="mh">0x0002</span><span class="p">,</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x6001</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//!</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//"</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">// #</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//$</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//%</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//&amp;</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//'</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//(</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//)</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//*</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//+</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//,</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//-</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//.</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">///</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 0</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 1</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 2</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 3</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 4</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 5</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 6</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 7</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 8</span>
    <span class="mh">0xd808</span><span class="p">,</span> <span class="c1">// 9</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//:</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//;</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//&lt;</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//=</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//&gt;</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//?</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//@</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// A</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// B</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// C</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// D</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// E</span>
    <span class="mh">0xd508</span><span class="p">,</span> <span class="c1">// F</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// G</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// H</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// I</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// J</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// K</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// L</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// M</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// N</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// O</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// P</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// Q</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// R</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// S</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// T</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// U</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// V</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// W</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// X</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// Y</span>
    <span class="mh">0xc508</span><span class="p">,</span> <span class="c1">// Z</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//[</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//]</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//^</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//_</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//`</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// a</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// b</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// c</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// d</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// e</span>
    <span class="mh">0xd608</span><span class="p">,</span> <span class="c1">// f</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// g</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// h</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// i</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// j</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// k</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// l</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// m</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// n</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// o</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// p</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// q</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// r</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// s</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// t</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// u</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// v</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// w</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// x</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// y</span>
    <span class="mh">0xc608</span><span class="p">,</span> <span class="c1">// z</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//{</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//|</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//}</span>
    <span class="mh">0xc004</span><span class="p">,</span> <span class="c1">//~</span>
    <span class="mh">0x0002</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// €</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ‚</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ƒ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// „</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// …</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// †</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ‡</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ˆ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ‰</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Š</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ‹</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Œ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ž</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ‘</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ’</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// “</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ”</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// •</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// –</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// —</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ˜</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ™</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// š</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ›</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// œ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ž</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ÿ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¡</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¢</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// £</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¤</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¥</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¦</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// §</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¨</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ©</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ª</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// «</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¬</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ­</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ®</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¯</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// °</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ±</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ²</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ³</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ´</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// µ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¶</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ·</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¸</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¹</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// º</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// »</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¼</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ½</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¾</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ¿</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// À</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Á</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Â</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ã</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ä</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Å</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Æ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ç</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// È</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// É</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ê</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ë</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ì</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Í</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Î</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ï</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ð</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ñ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ò</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ó</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ô</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Õ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ö</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ×</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ø</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ù</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ú</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Û</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ü</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Ý</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Þ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ß</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// à</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// á</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// â</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ã</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ä</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// å</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// æ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ç</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// è</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// é</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ê</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ë</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ì</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// í</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// î</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ï</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ð</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ñ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ò</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ó</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ô</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// õ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ö</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ÷</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ø</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ù</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ú</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// û</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ü</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ý</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// þ</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// ÿ</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0028</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0043</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0029</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x003c</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x003c</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="mh">0x002d</span><span class="p">,</span> <span class="mh">0x0000</span><span class="p">,</span> <span class="mh">0x0000</span><span class="p">,</span> <span class="mh">0x0000</span><span class="p">,</span> <span class="mh">0x0000</span><span class="p">,</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0028</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0052</span><span class="p">,</span> <span class="c1">//</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//!</span>
    <span class="mh">0x0029</span><span class="p">,</span> <span class="c1">//"</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// #</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//$</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//%</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//&amp;</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//'</span>
    <span class="mh">0x0075</span><span class="p">,</span> <span class="c1">//(</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//)</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//*</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//+</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//,</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//-</span>
    <span class="mh">0x002c</span><span class="p">,</span> <span class="c1">//.</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">///</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 0</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 1</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 2</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 3</span>
    <span class="mh">0x003e</span><span class="p">,</span> <span class="c1">// 4</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 5</span>
    <span class="mh">0x003e</span><span class="p">,</span> <span class="c1">// 6</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 7</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 8</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// 9</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//:</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//;</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">//&lt;</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//=</span>
    <span class="mh">0x0031</span><span class="p">,</span> <span class="c1">//&gt;</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//?</span>
    <span class="mh">0x002f</span><span class="p">,</span> <span class="c1">//@</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// A</span>
    <span class="mh">0x0034</span><span class="p">,</span> <span class="c1">// B</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// C</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">// D</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// E</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// F</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// G</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// H</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// I</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">// J</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// K</span>
    <span class="mh">0x0031</span><span class="p">,</span> <span class="c1">// L</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// M</span>
    <span class="mh">0x002f</span><span class="p">,</span> <span class="c1">// N</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// O</span>
    <span class="mh">0x0032</span><span class="p">,</span> <span class="c1">// P</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Q</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">// R</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// S</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// T</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// U</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// V</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// W</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">// X</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// Y</span>
    <span class="mh">0x0033</span><span class="p">,</span> <span class="c1">// Z</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//[</span>
    <span class="mh">0x002f</span><span class="p">,</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//]</span>
    <span class="mh">0x0034</span><span class="p">,</span> <span class="c1">//^</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//_</span>
    <span class="mh">0x0020</span><span class="p">,</span> <span class="c1">//`</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// a</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// b</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// c</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// d</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// e</span>
    <span class="mh">0x0041</span><span class="p">,</span> <span class="c1">// f</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// g</span>
    <span class="mh">0x0045</span><span class="p">,</span> <span class="c1">// h</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// i</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// j</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// k</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// l</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// m</span>
    <span class="mh">0x0078</span><span class="p">,</span> <span class="c1">// n</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// o</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// p</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// q</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// r</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// s</span>
    <span class="mh">0x0073</span><span class="p">,</span> <span class="c1">// t</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// u</span>
    <span class="mh">0x0073</span><span class="p">,</span> <span class="c1">// v</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// w</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// x</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// y</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">// z</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//{</span>
    <span class="mh">0x0061</span><span class="p">,</span> <span class="c1">//|</span>
    <span class="mh">0x0000</span><span class="p">,</span> <span class="c1">//}</span>
    <span class="mh">0x0065</span><span class="p">,</span> <span class="c1">//~</span>
    <span class="mh">0x0000</span>  <span class="c1">//</span>
<span class="p">};</span>

<span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">short</span> <span class="o">**</span><span class="n">__ctype_b_loc</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">static</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">short</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="n">b_loc_table</span><span class="p">;</span>
  <span class="k">return</span> <span class="o">&amp;</span><span class="n">ptr</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<p>After fixing this <code class="language-plaintext highlighter-rouge">import numpy</code> works, and that’s as far as I took things.</p>

<h3 id="some-notes-on-debugging-modules-with-dlmopen">Some notes on debugging modules with dlmopen</h3>

<p>By default, lldb can’t read the frames of stack traces from modules loaded via <code class="language-plaintext highlighter-rouge">dlmopen</code>. You’ll get a stack trace that looks something like this:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">frame</span> <span class="err">#</span><span class="mi">106</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb1f4</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">107</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb610</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">108</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb9ec</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">109</span><span class="o">:</span> <span class="mh">0x0000ffffedd7eff0</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">110</span><span class="o">:</span> <span class="mh">0x0000ffffedd7f388</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">111</span><span class="o">:</span> <span class="mh">0x0000ffffedd80568</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">112</span><span class="o">:</span> <span class="mh">0x0000fffff7e666e4</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span><span class="err">`</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">pybind_test</span><span class="p">(</span><span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Args</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">basic_string_view</span><span class="o">&lt;</span><span class="kt">char</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">char_traits</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;&gt;&gt;</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">)</span><span class="o">::</span><span class="err">$</span><span class="n">_0</span><span class="o">::</span><span class="k">operator</span><span class="p">()(</span><span class="k">this</span><span class="o">=</span><span class="mh">0x0000aaaaaab9a878</span><span class="p">)</span> <span class="k">const</span> <span class="n">at</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">cpp</span><span class="o">:</span><span class="mi">86</span><span class="o">:</span><span class="mi">5</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">113</span><span class="o">:</span> <span class="mh">0x0000fffff7e6667c</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span><span class="err">`</span><span class="kt">void</span> <span class="n">std</span><span class="o">::</span><span class="n">__invoke_impl</span><span class="o">&lt;</span><span class="kt">void</span><span class="p">,</span> <span class="n">pybind_test</span><span class="o">::</span><span class="n">pybind_test</span><span class="p">(</span><span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Args</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">basic_string_view</span><span class="o">&lt;</span><span class="kt">char</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">char_traits</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;&gt;&gt;</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">)</span><span class="o">::</span><span class="err">$</span><span class="n">_0</span><span class="o">&gt;</span><span class="p">((</span><span class="n">null</span><span class="p">)</span><span class="o">=</span><span class="n">__invoke_other</span> <span class="err">@</span> <span class="mh">0x0000ffffed9ff82f</span><span class="p">,</span> <span class="n">__f</span><span class="o">=</span><span class="mh">0x0000aaaaaab9a878</span><span class="p">)</span> <span class="n">at</span> <span class="n">invoke</span><span class="p">.</span><span class="n">h</span><span class="o">:</span><span class="mi">61</span><span class="o">:</span><span class="mi">14</span>
</code></pre></div></div>

<p>The way to fix this is:</p>
<ol>
  <li>Get at the base address in <code class="language-plaintext highlighter-rouge">/proc/&lt;proc&gt;/maps</code> for the module in question. The first number appears to be the start of the address space for that module.</li>
  <li>Load the module via <code class="language-plaintext highlighter-rouge">target modules add &lt;so_path&gt;</code></li>
  <li>Associate the symbols with the addresses via <code class="language-plaintext highlighter-rouge">target modules load --file "&lt;so_path&gt;" --slide 0x&lt;base_address&gt;</code></li>
</ol>

<p>Notably, you can only do this after module load, as the base address will be different every time.</p>

<p>After doing this, running <code class="language-plaintext highlighter-rouge">bt</code> will get you a slightly more sane <sup id="fnref:cpp" role="doc-noteref"><a href="#fn:cpp" class="footnote" rel="footnote">10</a></sup></p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">frame</span> <span class="err">#</span><span class="mi">141</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb1f4</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">_PyEval_EvalCodeWithName</span><span class="p">(</span><span class="n">_co</span><span class="o">=</span><span class="mh">0x0000fffff4bdb920</span><span class="p">,</span> <span class="n">globals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">locals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">argcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">kwnames</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">kwargs</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">kwcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">kwstep</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">defs</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">defcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">kwdefs</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">closure</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">qualname</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">)</span> <span class="n">at</span> <span class="n">ceval</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">4298</span><span class="o">:</span><span class="mi">14</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">142</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb610</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">PyEval_EvalCodeEx</span><span class="p">(</span><span class="n">_co</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">globals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">locals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">argcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">kws</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">kwcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">defs</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">defcount</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">kwdefs</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">closure</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">)</span> <span class="n">at</span> <span class="n">ceval</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">4327</span><span class="o">:</span><span class="mi">12</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">143</span><span class="o">:</span> <span class="mh">0x0000ffffeddbb9ec</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">PyEval_EvalCode</span><span class="p">(</span><span class="n">co</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">globals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">locals</span><span class="o">=&lt;</span><span class="n">unavailable</span><span class="o">&gt;</span><span class="p">)</span> <span class="n">at</span> <span class="n">ceval</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">718</span><span class="o">:</span><span class="mi">12</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">144</span><span class="o">:</span> <span class="mh">0x0000ffffedd7eff0</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">run_mod</span> <span class="p">[</span><span class="n">inlined</span><span class="p">]</span> <span class="n">run_eval_code_obj</span><span class="p">(</span><span class="n">locals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">globals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">co</span><span class="o">=</span><span class="mh">0x0000fffff4bdb920</span><span class="p">)</span> <span class="n">at</span> <span class="n">pythonrun</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">1166</span><span class="o">:</span><span class="mi">9</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">145</span><span class="o">:</span> <span class="mh">0x0000ffffedd7efb4</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">run_mod</span><span class="p">(</span><span class="n">mod</span><span class="o">=</span><span class="mh">0x0000ffffe80015c0</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="mh">0x0000fffff5447af0</span><span class="p">,</span> <span class="n">globals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">locals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">,</span> <span class="n">arena</span><span class="o">=</span><span class="mh">0x0000fffff5e8c7b0</span><span class="p">)</span> <span class="n">at</span> <span class="n">pythonrun</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">1188</span><span class="o">:</span><span class="mi">9</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">146</span><span class="o">:</span> <span class="mh">0x0000ffffedd7f388</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">PyRun_StringFlags</span><span class="p">(</span><span class="n">str</span><span class="o">=</span><span class="s">"</span><span class="se">\n</span><span class="s">import threading</span><span class="se">\n</span><span class="s">import time</span><span class="se">\n\n</span><span class="s">import numpy as np</span><span class="se">\n\n</span><span class="s">np.array(50)</span><span class="se">\n\n</span><span class="s">def do():</span><span class="se">\n</span><span class="s">  while True:</span><span class="se">\n</span><span class="s">      time.sleep(1)</span><span class="se">\n</span><span class="s">      print('This is a callback from a Python thread.')</span><span class="se">\n</span><span class="s">      </span><span class="se">\n</span><span class="s">t = threading.Thread(target = do)</span><span class="se">\n</span><span class="s">t.daemon = True</span><span class="se">\n</span><span class="s">t.start()</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">start</span><span class="o">=</span><span class="mi">257</span><span class="p">,</span> <span class="n">globals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">locals</span><span class="o">=</span><span class="mh">0x0000fffff54ce680</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">)</span> <span class="n">at</span> <span class="n">pythonrun</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">1061</span><span class="o">:</span><span class="mi">15</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">147</span><span class="o">:</span> <span class="mh">0x0000ffffedd80568</span> <span class="n">libpython3</span><span class="mf">.8</span><span class="p">.</span><span class="n">so</span><span class="mf">.1.0</span><span class="err">`</span><span class="n">PyRun_SimpleStringFlags</span><span class="p">(</span><span class="n">command</span><span class="o">=</span><span class="s">"</span><span class="se">\n</span><span class="s">import threading</span><span class="se">\n</span><span class="s">import time</span><span class="se">\n\n</span><span class="s">import numpy as np</span><span class="se">\n\n</span><span class="s">np.array(50)</span><span class="se">\n\n</span><span class="s">def do():</span><span class="se">\n</span><span class="s">  while True:</span><span class="se">\n</span><span class="s">      time.sleep(1)</span><span class="se">\n</span><span class="s">      print('This is a callback from a Python thread.')</span><span class="se">\n</span><span class="s">      </span><span class="se">\n</span><span class="s">t = threading.Thread(target = do)</span><span class="se">\n</span><span class="s">t.daemon = True</span><span class="se">\n</span><span class="s">t.start()</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="mh">0x0000000000000000</span><span class="p">)</span> <span class="n">at</span> <span class="n">pythonrun</span><span class="p">.</span><span class="n">c</span><span class="o">:</span><span class="mi">486</span><span class="o">:</span><span class="mi">9</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">148</span><span class="o">:</span> <span class="mh">0x0000fffff7e666e4</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span><span class="err">`</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">pybind_test</span><span class="p">(</span><span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Args</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">basic_string_view</span><span class="o">&lt;</span><span class="kt">char</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">char_traits</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;&gt;&gt;</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">)</span><span class="o">::</span><span class="err">$</span><span class="n">_0</span><span class="o">::</span><span class="k">operator</span><span class="p">()(</span><span class="k">this</span><span class="o">=</span><span class="mh">0x0000aaaaaab9a878</span><span class="p">)</span> <span class="k">const</span> <span class="n">at</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">cpp</span><span class="o">:</span><span class="mi">86</span><span class="o">:</span><span class="mi">5</span>
    <span class="n">frame</span> <span class="err">#</span><span class="mi">149</span><span class="o">:</span> <span class="mh">0x0000fffff7e6667c</span> <span class="n">pybind_test</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span><span class="err">`</span><span class="kt">void</span> <span class="n">std</span><span class="o">::</span><span class="n">__invoke_impl</span><span class="o">&lt;</span><span class="kt">void</span><span class="p">,</span> <span class="n">pybind_test</span><span class="o">::</span><span class="n">pybind_test</span><span class="p">(</span><span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Args</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">basic_string_view</span><span class="o">&lt;</span><span class="kt">char</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">char_traits</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;&gt;&gt;</span> <span class="k">const</span><span class="o">&amp;</span><span class="p">)</span><span class="o">::</span><span class="err">$</span><span class="n">_0</span><span class="o">&gt;</span><span class="p">((</span><span class="n">null</span><span class="p">)</span><span class="o">=</span><span class="n">__invoke_other</span> <span class="err">@</span> <span class="mh">0x0000ffffed9ff82f</span><span class="p">,</span> <span class="n">__f</span><span class="o">=</span><span class="mh">0x0000aaaaaab9a878</span><span class="p">)</span> <span class="n">at</span> <span class="n">invoke</span><span class="p">.</span><span class="n">h</span><span class="o">:</span><span class="mi">61</span><span class="o">:</span><span class="mi">14</span>
</code></pre></div></div>

<p>If you are in the same situation, here’s a script written by ChatGPT to do this. Use via <code class="language-plaintext highlighter-rouge">command script import load_all_slid_modules.py</code> and <code class="language-plaintext highlighter-rouge">load_all_slid_modules</code>. Notably, the script doesn’t work well across restarts, it tries to avoid reloading modules that it already saw, but this means things don’t work when the modules are reset. Pardon the excess emoji, I left them in so it’s obvious that I didn’t write it.</p>

<details>
  <summary><strong>load_all_slid_modules.py</strong></summary>
  <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">lldb</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">re</span>

<span class="k">def</span> <span class="nf">__lldb_init_module</span><span class="p">(</span><span class="n">debugger</span><span class="p">,</span> <span class="n">internal_dict</span><span class="p">):</span>
    <span class="n">debugger</span><span class="p">.</span><span class="n">HandleCommand</span><span class="p">(</span><span class="s">'command script add -f load_all_slid_modules.load_all_slid_modules load_all_slid_modules'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"✅ LLDB command 'load_all_slid_modules' installed"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">load_all_slid_modules</span><span class="p">(</span><span class="n">debugger</span><span class="p">,</span> <span class="n">command</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">internal_dict</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">target</span> <span class="o">=</span> <span class="n">debugger</span><span class="p">.</span><span class="n">GetSelectedTarget</span><span class="p">()</span>
    <span class="n">process</span> <span class="o">=</span> <span class="n">target</span><span class="p">.</span><span class="n">GetProcess</span><span class="p">()</span>
    <span class="n">pid</span> <span class="o">=</span> <span class="n">process</span><span class="p">.</span><span class="n">GetProcessID</span><span class="p">()</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="sa">f</span><span class="s">"/proc/</span><span class="si">{</span><span class="n">pid</span><span class="si">}</span><span class="s">/maps"</span><span class="p">,</span> <span class="s">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">maps</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">readlines</span><span class="p">()</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="n">result</span><span class="p">.</span><span class="n">PutCString</span><span class="p">(</span><span class="sa">f</span><span class="s">"❌ Failed to read /proc/</span><span class="si">{</span><span class="n">pid</span><span class="si">}</span><span class="s">/maps: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="k">return</span>

    <span class="n">seen</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="n">added</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">maps</span><span class="p">:</span>
        <span class="k">if</span> <span class="s">"r-xp"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">line</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">m</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s">"([0-9a-f]+)-[0-9a-f]+ r-xp .*? (/.+\.so(?:\.\d+)*)(?: |$)"</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">m</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">base_str</span><span class="p">,</span> <span class="n">path</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="n">groups</span><span class="p">()</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">path</span><span class="p">):</span>
            <span class="k">continue</span>

        <span class="n">base_addr</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">base_str</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
        <span class="n">real_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">realpath</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>

        <span class="n">key</span> <span class="o">=</span> <span class="p">(</span><span class="n">real_path</span><span class="p">,</span> <span class="n">base_addr</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">seen</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>

        <span class="n">cmd</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'target modules add "</span><span class="si">{</span><span class="n">real_path</span><span class="si">}</span><span class="s">"'</span>
        <span class="n">debugger</span><span class="p">.</span><span class="n">HandleCommand</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>

        <span class="n">cmd</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'target modules load --file "</span><span class="si">{</span><span class="n">real_path</span><span class="si">}</span><span class="s">" --slide 0x</span><span class="si">{</span><span class="n">base_addr</span><span class="si">:</span><span class="n">x</span><span class="si">}</span><span class="s">'</span>
        <span class="n">debugger</span><span class="p">.</span><span class="n">HandleCommand</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
        <span class="n">result</span><span class="p">.</span><span class="n">PutCString</span><span class="p">(</span><span class="sa">f</span><span class="s">"✅ Added: </span><span class="si">{</span><span class="n">real_path</span><span class="si">}</span><span class="s"> at 0x</span><span class="si">{</span><span class="n">base_addr</span><span class="si">:</span><span class="n">x</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
        <span class="n">added</span> <span class="o">+=</span> <span class="mi">1</span>

    <span class="k">if</span> <span class="n">added</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">result</span><span class="p">.</span><span class="n">PutCString</span><span class="p">(</span><span class="s">"🟢 No new slid modules found to load."</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">result</span><span class="p">.</span><span class="n">PutCString</span><span class="p">(</span><span class="sa">f</span><span class="s">"🎯 Loaded </span><span class="si">{</span><span class="n">added</span><span class="si">}</span><span class="s"> new module(s)."</span><span class="p">)</span>
</code></pre></div>  </div>
</details>

<p>Hopefully either (A) this post gets enough reach that searches for “lldb dlmopen no symbols” point to this script or (B) LLVM adds support for doing this automatically, and others don’t have to be as confused as I was.</p>

<p>I have no idea how to do this in gdb but <a href="https://github.com/bminor/binutils-gdb/commit/8d56636a0ecbe6c38bf52b0683326ee21693c548">they apparently silently fixed it a few years ago, in gdb 13.1</a>. For posterity, I’m running lldb 18.1.8 which is fairly modern.</p>

<h3 id="safely-calling-python-code-from-other-threads">Safely calling python code from other threads</h3>

<p>Two steps are needed.</p>
<ol>
  <li>In the main thread, call <code class="language-plaintext highlighter-rouge">PyThreadState *main_tstate = PyEval_SaveThread();</code> once.</li>
  <li>Any time after that, wrap your Python API using code via
    <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PyGILState_STATE</span> <span class="n">g</span> <span class="o">=</span> <span class="n">PyGILState_Ensure</span><span class="p">();</span>
<span class="n">PyRun_SimpleString</span><span class="p">(</span><span class="s">"print('Hello, world.')"</span><span class="p">);</span>
<span class="n">PyGILState_Release</span><span class="p">(</span><span class="n">g</span><span class="p">);</span>
</code></pre></div>    </div>
    <p>That’s it. Properly destructing and knowing when <code class="language-plaintext highlighter-rouge">PyEval_RestoreThread</code> is needed left as an exercise to the reader.</p>
  </li>
</ol>

<h3 id="automagically-shimming-symbols-from-another-module">Automagically shimming symbols from another module</h3>

<p>Use <a href="https://en.wikipedia.org/wiki/X_macro">X macros</a>. X macros are my favorite dumb C trick whenever I have to do different things on the same list of symbols multiple times (like defining+declaring a bunch of variables, making enum string conversions, etc, etc).</p>

<p>In this case, I first defined a list of symbols like so</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define X_PYTHON_API \
    X_PY(Py_InitializeEx) \
    X_PY(Py_Finalize) \
    X_PY(Py_IsInitialized) \
    X_PY(PyRun_SimpleStringFlags) \
    ...
</span></code></pre></div></div>
<p>then in the header, declared a bunch of function pointers</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">pybind_test</span> <span class="o">:</span> <span class="k">public</span> <span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Base</span> <span class="p">{</span>
<span class="nl">public:</span>
  <span class="p">...</span>
<span class="nl">private:</span>
  <span class="c1">// for each symbol, create a function pointer of the same type as in the Python API</span>
  <span class="cp">#define X_PY(f) decltype(::f)* f = nullptr;
</span>  <span class="n">X_PYTHON_API</span>
  <span class="cp">#undef X_PY
</span><span class="p">}</span>
</code></pre></div></div>
<p>and in the constructor, initialized them</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pybind_test</span><span class="o">::</span><span class="n">pybind_test</span><span class="p">(</span><span class="k">const</span> <span class="n">Args</span> <span class="o">&amp;</span><span class="n">args</span><span class="p">,</span>
                         <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string_view</span><span class="o">&gt;</span> <span class="o">&amp;</span><span class="n">name_override</span><span class="p">)</span>
    <span class="o">:</span> <span class="n">unit</span><span class="o">::</span><span class="n">pybind_test</span><span class="o">::</span><span class="n">Base</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="n">name_override</span><span class="p">),</span> <span class="n">pub</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">pub</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">void</span> <span class="o">*</span><span class="n">pyhandle</span> <span class="o">=</span> <span class="n">dlmopen</span><span class="p">(</span><span class="n">LM_ID_NEWLM</span><span class="p">,</span> <span class="s">"libpython_shim.so"</span><span class="p">,</span>
                           <span class="n">RTLD_LAZY</span> <span class="o">|</span> <span class="n">RTLD_LOCAL</span> <span class="o">|</span> <span class="n">RTLD_DEEPBIND</span><span class="p">);</span>
  <span class="p">...</span>
<span class="c1">// For each symbol, find it on the shared object, then cast it to the proper type</span>
<span class="c1">// and store on the object</span>
<span class="cp">#define X_PY(f)                                                                \
  this-&gt;f = reinterpret_cast&lt;decltype(this-&gt;f)&gt;(dlsym(pyhandle, #f));          \
  if (!this-&gt;f) {                                                              \
    const char *error = dlerror();                                             \
    BASIS_LOG_FATAL("error while getting '" #f "': {}'", error);               \
  }
</span>  <span class="n">X_PYTHON_API</span>
<span class="cp">#undef X_PY
</span><span class="p">}</span>
</code></pre></div></div>

<p>Now a simple call to <code class="language-plaintext highlighter-rouge">Py_InitializeEx(0)</code> will resolve to the member function pointer, transparently. If you forget to add a symbol to the list, you’ll get a linker error <strong>as long as you didn’t link Python</strong>. Don’t link Python in this case, we never ever want to confuse things and run the Python API on something other than our handles. If I were to put the shim dll into production I’d do the same thing with the forwarded symbols.</p>

<h3 id="what-to-do-about-pybind">What to do about pybind.</h3>

<p>I wouldn’t use pybind for this, it’s too hard. Sticking more about this under another expandable section as I think most people won’t care.</p>
<details>
  <!-- <summary markdown="span">**load_all_slid_modules.py**</summary> -->
  <p>I figured that I could “just” extract the symbols I needed from pybind, and redirect them to a wrapper that delegated to the proper python handle. I even had some fun assembly code to do it. This didn’t work for three reasons:</p>

  <ol>
    <li>I’m unsure if I could properly associate a pybind call with the python handle it needed. I <em>think</em> I could get clever with thread local variables and make it work, but I’m worried about other threads that I don’t create. I don’t want to have to wrap all thread creation to “inherit” the python handle, at least at this point.</li>
    <li>Static variables are in use here. The clever redirection I had to delegate wouldn’t work.</li>
    <li>I wouldn’t be able to use any of the template helpers.
I might still be able to get this to work with careful use of macros, but it would be safer just to fork pybind, which I’m not going to do.</li>
  </ol>

  <h3 id="what-then-instead">What then instead?</h3>

  <p>For Basis, the API surface between a Unit and the framework surrounding it is really really small. As best as I can tell I need a few things for generic Python bindings:</p>
  <ol>
    <li>To be able to convert a byte buffer into a deserialized python message (needed to accept messages from python)p</li>
    <li>To do the convert a python message into a serialized byte buffer</li>
    <li>To be able to create an <code class="language-plaintext highlighter-rouge">object</code> to hold the inputs and outputs from each callback</li>
    <li>To be able to call each callback with all of the above</li>
    <li>Logging framework injection</li>
  </ol>

  <p>Optional bonuses for later:</p>
  <ol>
    <li>Define a base class for python Units to inherit from with the API for your Unit
      <ul>
        <li>This is not strictly neccessary, but will help with tooling</li>
      </ul>
    </li>
    <li>Be able to query python objects directly for members or run small lambdas on them to get at the members for synchronization
      <ul>
        <li>Mainly a performance win, so that we can avoid extra deserializations. For first pass, we can deserialize incoming messages first as cpp (to be able to synchronize on a member variable or similar), and then as py (to pass to the Unit).</li>
      </ul>
    </li>
  </ol>

  <p>None of this requires any crazy features. Some of the helpers pybind provides are really nice, but in this case it’s not needed.</p>
</details>

<h3 id="TLDR">TLDR</h3>

<ol>
  <li>Use <code class="language-plaintext highlighter-rouge">dlmopen</code> per python interpreter you want to run</li>
  <li>Inject a few <code class="language-plaintext highlighter-rouge">pthread</code> and string/locale related functions</li>
  <li>It works, at least until you find some other thread local storage related crash</li>
</ol>

<h1 id="well-what-now">Well, what now?</h1>

<p>Overall, I would <em>not</em> recommend doing this yourself. From what I can tell, once you fix thread local variable creation and locale related code, things look fine, but when things go wrong it’s a royal pain to debug. I’m not sure the extent at which things could break here. Past the initial experiment I mainly did this because I thought it could be done (also, because some of the stuff I learned would be useful to others using <code class="language-plaintext highlighter-rouge">dlmopen</code> for the first time). There are no advantages here other than slightly better isolation between modules and the ability to work on older Python versions.</p>

<p>It also looks like glibc also only supports 16 namespaces, so you’d be limited to 15 or so Python interpreters with this method, though <a href="https://news.ycombinator.com/item?id=32926306">girfan claims you can patch it for more</a>.</p>

<p><del>I’m going to clean up the code for this and post it as a Draft PR, for posterity.</del> PR is up <a href="https://github.com/basis-robotics/basis/pull/70">here</a>. For the real implementation of this I’ll probably just use Python3.12 subinterpreters, which have independent GIL. <code class="language-plaintext highlighter-rouge">rospy</code> messages load just fine in newer versions of Python, with a little bit of path trickery, so I really shouldn’t need to use 3.8. Maybe I’ll actually get around to writing the bindings I was trying to work on, rather than getting sidetracked for a week on something nobody asked for.</p>

<p>As I said at the top, I’ll probably put out more blog posts, later. The next one will probably be on the acutal Python bindings I’m going to write.<sup id="fnref:footnotes" role="doc-noteref"><a href="#fn:footnotes" class="footnote" rel="footnote">11</a></sup></p>

<hr />
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:who" role="doc-endnote">
      <p>won’t post who here just as policy of “my viewpoints are my own and not associcated with my employer, but go check my LinkedIn if you want to know”<sup id="fnref:really" role="doc-noteref"><a href="#fn:really" class="footnote" rel="footnote">12</a></sup> <a href="#fnref:who" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:guitar" role="doc-endnote">
      <p>no, seriously. the guys at the music store barely believe it. but a “free” $2k amp has turned into a $300 tune up for the amp (it deserved it even if I sold it), a guitar, a speaker to hook it up to, etc. most expensive free thing I’ve ever gotten, beyond my cats. <a href="#fnref:guitar" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:processes" role="doc-endnote">
      <p>somehow everyone I’ve spoken to about Basis is surprised about this - I should have made it more obvious. it’s nowhere in the docs?! <a href="#fnref:processes" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pyenv" role="doc-endnote">
      <p>even this is a little suspect, the py3.8 docs have a <a href="https://docs.python.org/3.8/c-api/init.html#bugs-and-caveats">bunch of caveats</a> for exactly what state is new. <a href="#fnref:pyenv" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:linux" role="doc-endnote">
      <p>okay, full disclosure, other people have called me a Linux expert, what do I know? <a href="#fnref:linux" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:linkmap" role="doc-endnote">
      <p>the man docs refer to <code class="language-plaintext highlighter-rouge">link-map lists</code> and <code class="language-plaintext highlighter-rouge">namespaces</code>, but this is basically the gist of things. you can also query an existing handle for its id to load another shared object into it, but it has weird effects around sharing symbols. <a href="#fnref:linkmap" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:static" role="doc-endnote">
      <p>as I write this, I can actually think of a few ways to solve this, but it’s not worth my time right now. <a href="#fnref:static" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:smell" role="doc-endnote">
      <p>you don’t have to understand what’s broken in this case to get a gut feeling that it won’t work right <a href="#fnref:smell" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:sos" role="doc-endnote">
      <p>i tried to have an isolated shim and load python separately - it didn’t work, I got missing symbol errors later on down the line when python itself used dlopen on a python module. <a href="https://codebrowser.dev/glibc/glibc/dlfcn/dlmopen.c.html#55"><code class="language-plaintext highlighter-rouge">dlmopen</code> explicitly doesn’t support RTLD_GLOBAL</a>, which i feel is a bug - it should instead allow symbol visibility to other modules loaded in the same namespace. <a href="#fnref:sos" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:cpp" role="doc-endnote">
      <p>if you count cpp stack traces that wrap multiple times sane, and if you count 150 frame deep stack traces sane <a href="#fnref:cpp" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:footnotes" role="doc-endnote">
      <p>also - i discovered how to use footnotes for this post. hopefully i didn’t go too overboard. <a href="#fnref:footnotes" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:really" role="doc-endnote">
      <p>actually they might want the assocation, who knows, will let them speak up if they do :) <a href="#fnref:really" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[First off…]]></summary></entry><entry><title type="html">Basis is shutting down</title><link href="basisrobotics.tech/2025/01/08/postmortem/" rel="alternate" type="text/html" title="Basis is shutting down" /><published>2025-01-08T00:00:00+00:00</published><updated>2025-01-08T00:00:00+00:00</updated><id>basisrobotics.tech/2025/01/08/postmortem</id><content type="html" xml:base="basisrobotics.tech/2025/01/08/postmortem/"><![CDATA[<p>Basis as a company is shutting down. Thomas and I gave it a good shot but - we have no funding, and after our release, not one person tried our software.</p>

<h1 id="good-software-bad-company">Good software, bad company</h1>

<h2 id="building-backwards">Building backwards</h2>

<p>What happened? Basically, what we have are the pretty good bones of a distributed framework, but there’s no way we can see this working economically. I built everything backwards - I had an idea that I wanted to build, built it, and then tried to get others interested. That’s fine for OSS (I’m happy I built it, and I’ll use it), but not great for sustainting a business.</p>

<p>One of the VCs we talked to specifically asked where our plan for building a community was. At the time I didn’t get it (why would I waste runway on that?! who would join a community for a thing they couldn’t try?), but now I see that he was looking for some form of validation - I’d assumed that releasing the framework would get people to try it, rather than paving the way for success first. Conventional startup wisdom seems to be to find PMF first, then build. We did the reverse. (Matt! You were right, we just didn’t get it at the time.)</p>

<h2 id="focusing-on-the-wrong-thing">Focusing on the wrong thing</h2>

<p>Late in the process, we realized that we were somewhat barking up the wrong tree. We’d focused very much on deterministic replay and testability as the framework’s key features. In our experience looking at larger robotics companies, these are important to have. However - our initial customers wouldn’t care about those things. They care about how quickly they can get off the ground and get features out the door.</p>

<p>There’s a reason people use ROS - it’s really great for getting your robot prototyped. We could have focused our “marketing” on doing that, but even better - this was an area we are better than ROS in, in several ways (not having to use a custom build system, easily supporting new platforms, abstracting away system software concerns from your roboticists, etc). This was another bit of feedback we got from a VC - they recognized that ROS is a problem, but didn’t see how believe it was slowing down their robotics companies. We should have swapped our marketing much sooner, especially as we got several comments on release that “something easier get started with than ROS” was a great idea.</p>

<h2 id="difficult-to-capture-initial-customers">Difficult to capture initial customers</h2>

<p>There’s another problem here - who <em>are</em> our initial customers? We’d had two ideal companies in mind: fresh companies with not much code, and companies stuck on ROS1 but not yet moved to ROS2. In retrospect, these are difficult customers to find.</p>

<p>The first group isn’t super loud about existing, and moreover, they are busy and unlikely to want to bet their company on new tech, unless they really know what they want (in our entire time of searching, we found one of these companies, who is/was still a promising lead as of this post, but see the next section for how this doesn’t help…).</p>

<p>The latter group is just too damn hard to capture. Ignoring the fact they have a lower tolerance for missing features, they more than likely have started to swap to ROS2 anyhow. They won’t want to roll back that move, and aren’t likely to want to immediately move a second time after making the jump. We talked with a few companies still on ROS1 and “happily” there - if fit their needs, they were fine with eating the maintinence cost of a fork, and didn’t need anything we could provide. Beyond all this, it’s a huge ask to get companies to just try our stuff out. Robots are pretty specialized, and we don’t have a deep enough back cataloge of modules to be able to just toss some drivers and a planning algorithm together and get a quick prototype working for a new user wanting to try things. We probably could have done better here, and made more compelling demos.</p>

<h2 id="economics">Economics</h2>

<p>Lastly - this whole thing never actually made sense from an economic perspective. The whole idea was that companies should be willing to pay for a better middleware. So far…I haven’t seen any evidence of it, though. People are willing to pay for a lot of things that have free alternatives, and willing to pay for robotics software, too. Foxglove appears to be succeeding in the visualization/data management space. For some reason, people are willing to throw millions a year at Applied Intuition for simulation.</p>

<p>Why can’t middleware be the same? We theorized that using Basis could save at least a system software hire, that has to be worth something, right? Turns out, not so much. I have some theories around this, but I’m guessing it’s just too scary to move to a new framework, even for free. We had one friend in the industry suggest that companies might pay $1MM/year for a performance focused framework (I wish it were true, but nobody has come up to me with promises saying they would). Our best plan was to get a little bit of money short term in (discounted) contract integration work, and then make it up years down the line with some sort of game-engine-like fee structure. This still has a few big holes:</p>

<ol>
  <li>How do we structure the contract/license so that customers are protected from Basis Robotics having bad behavior down the line? Putting on my customer hat, I’d be very afraid of basing my business on someone else’s platform - they can screw me later and there’s nothing I can do! This happens a fair amount in the mobile space w/Apple and Google. Those companies deal with it, but smaller developers often feel the pain.</li>
  <li>How is pricing scaled? If it’s based on revenue, we lose out on all money from the next Cruise or other company that <em>has</em> a ton of money, but makes zero revenue. If it’s based on robot count or developer count, it’s very hard to make a one size fits all contract. This sucks from the point of view of having clear pricing.</li>
</ol>

<p>Even if we find a price structure and contract that fits, the math on profit is grim. And given we don’t have evidence people will pay in the short term, that rules out bootstrapping.</p>

<h2 id="whats-happening-to-the-framework">What’s happening to the framework?</h2>

<p>Basis as a piece of software will continue to exist, as a hobby/Open Source project. I’ll make the license plain Apache 2.0, and open the <code class="language-plaintext highlighter-rouge">determinsitic_replay</code> repo that makes deterministic playback possible (and eventually pull it forward to be compatible with <code class="language-plaintext highlighter-rouge">main</code>, integrate it within the <code class="language-plaintext highlighter-rouge">basis</code> repo, etc, etc). I’ll still work on my little test robot, for fun. The next thing to do is to integrate the LIDAR I bought and put some SLAM algo on top - there’s a few out there, but once the <a href="https://github.com/SLAM-Handbook-contributors/slam-handbook-public-release">SLAM Handbook</a> releases Part 2, I might try to do my own.</p>

<h1 id="why-didnt-we">Why didn’t we…</h1>

<h2 id="get-angel-funding">Get Angel funding?</h2>

<p>Angel funding won’t solve the user problem. Again, not a single person tried our software after we released it. Maybe we could have done so and paid a marketing genius to help us out. Beyond that, if we’re going to get funding I want enough that I can draw a small salary to pay rent. Before that it’s not really worth it, if we had enough traction I’d much rather have borrowed/gotten investment from friends/family. The numbers are low enough for angel funding that I can make it back in a short time at a FAANG or AI company to make them whole, if we borrowed and busted.</p>

<h2 id="pivot">Pivot?</h2>

<p>We could certainly pivot to making a robot, instead. I have a few ideas (putting it out there - some sort of Foundational Model based handyman robot with interchangable hands/fingers will come some day, I think). I think there’s also space for improvement for robotics deployment and telemetry (though the space isn’t as empty as middleware was). But I’m really loathe to spend another three to six months grinding and fundraising (again) - my savings are drained (they were low already due to some poor choices with stock options from Embark Trucks). We aren’t going to get kicked out of our apartment or anything - my wife has savings and income, but I need a good reason to start dipping into her savings as well.</p>

<h2 id="build-on-top-of-ros">Build on top of ROS?</h2>

<p>I don’t want to. I don’t like their development model: everything is way too distributed, designed by committee, it’s impossible to know who really owns what or what is good quality. <a href="https://github.com/ros2/geometry2/pull/741">My PR to tf2 removing extra Matrix-&gt;Quaternion conversions still hasn’t merged…</a>. Beyond that, I don’t think the user story really makes sense. The static analysis and replay benefits that Basis has won’t work for things like MoveIt or Nav2 - they are too large, I’d never convince them to use my bindings on top, and the API surface is too large to cover as a fork. It might have been a solid strategy to start with (one of the YC partners asked why we weren’t), but at this point - too late. Maybe someday I’ll go back and add a ROS backend to the code generator.</p>

<h2 id="integrate-ai">Integrate AI?</h2>

<p>This would have probably been a solid play to get VC money, if I were more dishonest. I did wrack my brain for how AI could help, and at best I could come up with is “declaring all of each unit’s inputs and outputs makes it easier for AIs to understand the codebase”, which is true for humans as well. I think there is probably a need for a good production framework for working with Foundational Models, but I’d need to invest in more hardware and pay a platform to train models on (as well as skilling up on modern AI, which I suppose I’ll have to do eventually, anyhow).</p>

<h1 id="conclusion">Conclusion</h1>

<p>I learned a hell of a lot out of this. It was even more stressful than I expected, and we never even had money change hands. I went from being super nervous at every pitch to being pretty relaxed and happy to talk about what we were building. Got a whole lot better at networking at conventions, gave my first talk in front of other companies at an event. I got to set up the bones of a company from scratch. I got to get the idea that’s been knocking around my head for the past three or four years out of it and into code. I feel lucky that I had the opportunity for all of this and I’m not unhappy for trying.</p>

<p>Given the opportunity, I’d do it again.</p>

<h2 id="whats-next">What’s next?</h2>

<p>It sounds like Thomas is taking some time off - he deserves it. It’s been a fun time, and I’m glad to have had him at my side.</p>

<p>I’m now open to contract and full time work. You can see the entire corpus of what I’ve worked on over the past 7 months on our github (please keep in mind that normally I’d do a bit better with tests/docs!). I’d love to stay in robotics but open to other areas as well. Please reach out at kyle@basisrobotics.tech</p>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[Basis as a company is shutting down. Thomas and I gave it a good shot but - we have no funding, and after our release, not one person tried our software.]]></summary></entry><entry><title type="html">Building a Robot on Basis 02 - Software</title><link href="basisrobotics.tech/2024/11/24/basis-robot-02-software/" rel="alternate" type="text/html" title="Building a Robot on Basis 02 - Software" /><published>2024-11-24T00:00:00+00:00</published><updated>2024-11-24T00:00:00+00:00</updated><id>basisrobotics.tech/2024/11/24/basis-robot-02-software</id><content type="html" xml:base="basisrobotics.tech/2024/11/24/basis-robot-02-software/"><![CDATA[<p>I’ve spent the past few weeks working on a small robot to both be able to give demos with and exercise our code. This post is about the current software architecture for the robot.</p>

<ul>
  <li><a href="/2024/11/22/basis-robot-01-hardware/">Part 01 - Hardware</a></li>
  <li><a href="/2024/11/24/basis-robot-02-software/">Part 02 - Software</a> (You’re here!)</li>
  <li>Part 03 - tf2 support and LiDAR</li>
</ul>

<p>After getting the hardware working, I moved on to the software. This required <a href="https://github.com/basis-robotics/basis/pull/64/files">a few small changes to the core framework</a> (mostly fixing CMake technicalities), but nothing crazy.</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/v8CYzripJm0?si=F6h22o1RYu7xqred" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>I can now move the robot around with an wireless controller! The left stick and bumpers control the wheels and the right stick controls the servos.</p>

<h1 id="the-architecture">The Architecture:</h1>
<pre class="mermaid">
---
config:
  flowchart:
    nodeSpacing: 5
    subGraphTitleMargin:
      bottom: 10
  defaultRenderer: elk
  elk:
      mergeEdges: True
---
graph LR
  %%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
  subgraph unit_/freenove/rpi_freenove_mecanum_driver["/freenove/rpi_freenove_mecanum_driver"]
    handler_/freenove/rpi_freenove_mecanum_driver::Update[["Update()
100Hz"]]
  end

  subgraph unit_/freenove/rpi_freenove_servo_driver["/freenove/rpi_freenove_servo_driver"]
    handler_/freenove/rpi_freenove_servo_driver::Update[["Update()
100Hz"]]
  end

  subgraph unit_/freenove/rpi_libcamera_driver["/freenove/rpi_libcamera_driver"]
    handler_/freenove/rpi_libcamera_driver::OnCameraImage[["OnCameraImage()
30Hz"]]
  end

  subgraph unit_/freenove/joystick_driver["/freenove/joystick_driver"]
    handler_/freenove/joystick_driver::Tick[["Tick()
20Hz"]]
  end

  subgraph unit_/foxglove/foxglove["/foxglove/foxglove"]
    handler_/foxglove/foxglove:::hidden
  end
handler_/freenove/rpi_freenove_servo_driver::Update::/servo/1/request_degrees:::hidden x--/servo/1/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/rpi_freenove_servo_driver::Update::/servo/0/request_degrees:::hidden x--/servo/0/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/joystick_driver::Tick --/user_inputs--&gt; handler_/freenove/rpi_freenove_mecanum_driver::Update
handler_/freenove/joystick_driver::Tick --/user_inputs--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/rpi_libcamera_driver::OnCameraImage --/camera/rgb--x /camera/rgb::handler_/freenove/rpi_libcamera_driver::OnCameraImage:::hidden
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/1/current_degrees--x /servo/1/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/0/current_degrees--x /servo/0/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
handler_/freenove/rpi_freenove_mecanum_driver::Update --/motor_state--x /motor_state::handler_/freenove/rpi_freenove_mecanum_driver::Update:::hidden
</pre>

<p>This is a pretty straightforward architecture, for now. We run joystick input, allowing it to control both the servos the camera is mounted on as well as the wheels. Later, we’ll move the joystick input to the wheels to instead be an input to some sort of planning stick.</p>

<p>This graph was generated with <code class="language-plaintext highlighter-rouge">basis launch --mermaid</code> - it does a dry run, outputting information about the launch in <a href="https://mermaid.js.org/">mermaid</a>. This is really useful - I can copy/paste directly into a blog post or github markdown document. The PR for this will be merged soon.</p>

<h1 id="the-code">The code</h1>

<h2 id="rpi_libcamera_driver">rpi_libcamera_driver</h2>

<figure class="highlight">
  <pre><code class="language-yaml" data-lang="yaml"><span class="na">args</span><span class="pi">:</span>
  <span class="na">device</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">help</span><span class="pi">:</span> <span class="s">The device to capture from</span>
    <span class="na">optional</span><span class="pi">:</span> <span class="s">True</span>
  
  <span class="na">width</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">uint32_t</span>
    <span class="na">default</span><span class="pi">:</span> <span class="m">1280</span>
  <span class="na">height</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">uint32_t</span>
    <span class="na">default</span><span class="pi">:</span> <span class="m">720</span>
  <span class="na">topic_namespace</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/camera</span>

<span class="na">threading_model</span><span class="pi">:</span>
  <span class="s">single</span>
<span class="na">cpp_includes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">foxglove/RawImage.pb.h</span>
  <span class="pi">-</span> <span class="s">image_conversion.h</span>

<span class="na">handlers</span><span class="pi">:</span>
  <span class="na">OnCameraImage</span><span class="pi">:</span>
    <span class="na">sync</span><span class="pi">:</span>
      <span class="c1"># All inputs are required (of which there are technically none)</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">all</span>
      <span class="c1"># Run at 30fps</span>
      <span class="na">rate</span><span class="pi">:</span> <span class="m">0.03333333333</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="pi">{{</span><span class="nv">args.topic_namespace</span><span class="pi">}}</span><span class="na">/rgb</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:foxglove.RawImage</span>
        <span class="na">inproc_type</span><span class="pi">:</span> <span class="s">image_conversion::Image</span></code></pre>
</figure>

<p>This unit implements <a href="https://libcamera.org/">libcamera</a> support. libcamera on raspberry pi does have a v4l2 interface, I sadly wasn’t able to get it working. There’s nothing special about this code, <a href="https://github.com/basis-robotics/basis_test_robot/tree/main/unit/rpi_libcamera_driver">you can see it here</a>. It’s based off of <a href="https://libcamera.org/guides/application-developer.html">the libcamera tutorial in their docs</a>. The only odd point was that <code class="language-plaintext highlighter-rouge">libcamera::formats::RGB888</code> appears to be <code class="language-plaintext highlighter-rouge">BGR</code> - I didn’t bother to track down why, I instead just swapped to requesting <code class="language-plaintext highlighter-rouge">BGR</code>.</p>

<p>Note: it’s not perfect that we run this unit at a fixed 30hz - ideally, this unit (and other driver-like units) can update freely on a thread, and publish at will. This requires a change to Basis (<code class="language-plaintext highlighter-rouge">sync: type: external</code>), which will be made in the next month.</p>

<h2 id="joystick_driver">joystick_driver</h2>

<figure class="highlight">
  <pre><code class="language-yaml" data-lang="yaml"><span class="na">threading_model</span><span class="pi">:</span>
  <span class="s">single</span>
<span class="na">cpp_includes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">basis_robot_input.pb.h</span>
<span class="na">handlers</span><span class="pi">:</span>
  <span class="na">Tick</span><span class="pi">:</span>
    <span class="na">sync</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">all</span>
      <span class="na">rate</span><span class="pi">:</span> <span class="m">0.05</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">/user_inputs</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:basis::robot::input::InputState</span></code></pre>
</figure>

<p>Again, very straightforward. I initially implemented this using <code class="language-plaintext highlighter-rouge">ioctl</code> and then switched to <a href="https://www.freedesktop.org/wiki/Software/libevdev/"><code class="language-plaintext highlighter-rouge">libevdev</code></a>. This really simplified controller access. <a href="https://github.com/basis-robotics/basis_test_robot/tree/main/unit/joystick_driver">See here.</a></p>

<h2 id="rpi_freenove_servo_driver">rpi_freenove_servo_driver</h2>

<figure class="highlight">
  <pre><code class="language-yaml" data-lang="yaml"><span class="na">args</span><span class="pi">:</span>
  <span class="na">i2c_device</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/dev/i2c-1"</span>
  <span class="na">address</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">int32_t</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">0x40</span>
  <span class="na">default_angle_0</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">float</span>
    <span class="na">help</span><span class="pi">:</span> <span class="s">the angle to start at</span>
    <span class="na">default</span><span class="pi">:</span> <span class="m">0</span>
  <span class="na">default_angle_1</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">float</span>
    <span class="na">help</span><span class="pi">:</span> <span class="s">the angle to start at</span>
    <span class="na">default</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">threading_model</span><span class="pi">:</span>
  <span class="s">single</span>
<span class="na">cpp_includes</span><span class="pi">:</span> 
  <span class="pi">-</span> <span class="s">google/protobuf/wrappers.pb.h</span>
  <span class="pi">-</span> <span class="s">basis_robot_input.pb.h</span>

<span class="na">handlers</span><span class="pi">:</span>
  <span class="na">Update</span><span class="pi">:</span>
    <span class="na">sync</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">all</span>
      <span class="c1"># Run at 100fps</span>
      <span class="na">rate</span><span class="pi">:</span> <span class="m">0.01</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="c1"># All three inputs here are optional - we will run at 100hz regardless of the messages we get in</span>
      <span class="na">/user_inputs</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:basis::robot::input::InputState</span>
        <span class="na">optional</span><span class="pi">:</span> <span class="s">True</span>
        <span class="na">cached</span><span class="pi">:</span> <span class="s">True</span>
      <span class="na">/servo/1/request_degrees</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:google::protobuf::DoubleValue</span>
        <span class="na">optional</span><span class="pi">:</span> <span class="s">True</span>
      <span class="na">/servo/0/request_degrees</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:google::protobuf::DoubleValue</span>
        <span class="na">optional</span><span class="pi">:</span> <span class="s">True</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="s2">"</span><span class="s">/servo/0/current_degrees"</span><span class="err">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:google::protobuf::DoubleValue</span>
      <span class="s2">"</span><span class="s">/servo/1/current_degrees"</span><span class="err">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:google::protobuf::DoubleValue</span></code></pre>
</figure>

<p>Finally - a little bit of complexity. This unit runs at 100hz, and picks up any inputs that were published since the last tick. We cache <code class="language-plaintext highlighter-rouge">/user_inputs</code> - it can run at a lower rate, and we’re okay with reusing the last joystick input for 5 ticks - nobody will notice the difference.</p>

<h3 id="doing-things-the-hard-way">Doing things the hard way</h3>

<p>The initial version of the servo looked something like this:</p>
<pre class="mermaid">
---
config:
  flowchart:
    nodeSpacing: 5
    subGraphTitleMargin:
      bottom: 10
  defaultRenderer: elk
  elk:
      mergeEdges: True
---
graph LR
  %%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
  subgraph unit_/freenove/rpi_freenove_servo_driver["/freenove/rpi_freenove_servo_driver"]
    handler_/freenove/rpi_freenove_servo_driver::OnInputs[["OnInputs()"]]
    handler_/freenove/rpi_freenove_servo_driver::RequestState0[["RequestState0()"]]
    handler_/freenove/rpi_freenove_servo_driver::RequestState1[["RequestState1()"]]
    handler_/freenove/rpi_freenove_servo_driver::Update[["Update()
100Hz"]]
  end
handler_/freenove/rpi_freenove_servo_driver::RequestState1::/servo/1/request_degrees:::hidden x--/servo/1/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::RequestState1
handler_/freenove/rpi_freenove_servo_driver::RequestState0::/servo/0/request_degrees:::hidden x--/servo/0/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::RequestState0
handler_/freenove/rpi_freenove_servo_driver::OnInputs::/user_inputs:::hidden x--/user_inputs--&gt; handler_/freenove/rpi_freenove_servo_driver::OnInputs
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/1/current_degrees--x /servo/1/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/0/current_degrees--x /servo/0/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
</pre>

<p>This required storing each input to the unit and is less performant than letting the framework handle it.</p>

<h3 id="optional-and-cached">Optional and Cached</h3>

<p>With <code class="language-plaintext highlighter-rouge">optional</code> and <code class="language-plaintext highlighter-rouge">cached</code>, the code is straightforward. <code class="language-plaintext highlighter-rouge">optional</code> lets a handler run without the tagged input. <code class="language-plaintext highlighter-rouge">cached</code> keeps an input around for future executions of the handler. You can use it to work around differences in publish rates while still having a single handler. In this example we use it for input, but another use might be for something like loading a map and publishing it at a low rate. The less often messages need published, the better.</p>

<pre class="mermaid">
---
config:
  flowchart:
    nodeSpacing: 5
    subGraphTitleMargin:
      bottom: 10
  defaultRenderer: elk
  elk:
      mergeEdges: True
---
graph LR
  %%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
  subgraph unit_/freenove/rpi_freenove_servo_driver["/freenove/rpi_freenove_servo_driver"]
    handler_/freenove/rpi_freenove_servo_driver::Update[["Update()
100Hz"]]
  end
handler_/freenove/rpi_freenove_servo_driver::Update::/user_inputs:::hidden x--/user_inputs--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/rpi_freenove_servo_driver::Update::/servo/1/request_degrees:::hidden x--/servo/1/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/rpi_freenove_servo_driver::Update::/servo/0/request_degrees:::hidden x--/servo/0/request_degrees--&gt; handler_/freenove/rpi_freenove_servo_driver::Update
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/1/current_degrees--x /servo/1/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
handler_/freenove/rpi_freenove_servo_driver::Update --/servo/0/current_degrees--x /servo/0/current_degrees::handler_/freenove/rpi_freenove_servo_driver::Update:::hidden
</pre>

<p>I’ll show the code here…</p>

<figure class="highlight">
  <pre><code class="language-cpp" data-lang="cpp"><span class="k">class</span> <span class="nc">rpi_freenove_servo_driver</span> <span class="o">:</span> <span class="k">public</span> <span class="n">unit</span><span class="o">::</span><span class="n">rpi_freenove_servo_driver</span><span class="o">::</span><span class="n">Base</span> <span class="p">{</span>
<span class="nl">public:</span>
  <span class="n">rpi_freenove_servo_driver</span><span class="p">(</span><span class="k">const</span> <span class="n">Args</span><span class="o">&amp;</span> <span class="n">args</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">optional</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string_view</span><span class="o">&gt;&amp;</span> <span class="n">name_override</span> <span class="o">=</span> <span class="p">{});</span>

  <span class="k">virtual</span> <span class="n">unit</span><span class="o">::</span><span class="n">rpi_freenove_servo_driver</span><span class="o">::</span><span class="n">Update</span><span class="o">::</span><span class="n">Output</span>
  <span class="n">Update</span><span class="p">(</span><span class="k">const</span> <span class="n">unit</span><span class="o">::</span><span class="n">rpi_freenove_servo_driver</span><span class="o">::</span><span class="n">Update</span><span class="o">::</span><span class="n">Input</span> <span class="o">&amp;</span><span class="n">input</span><span class="p">)</span> <span class="k">override</span><span class="p">;</span>

  <span class="n">PiPCA9685</span><span class="o">::</span><span class="n">PCA9685</span> <span class="n">pca</span><span class="p">;</span>

  <span class="k">static</span> <span class="kr">inline</span> <span class="k">constexpr</span> <span class="kt">size_t</span> <span class="n">NUM_SERVOS</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">array</span><span class="o">&lt;</span><span class="kt">double</span><span class="p">,</span> <span class="n">NUM_SERVOS</span><span class="o">&gt;</span> <span class="n">current_state</span> <span class="o">=</span> <span class="p">{};</span>
  <span class="n">std</span><span class="o">::</span><span class="n">array</span><span class="o">&lt;</span><span class="kt">double</span><span class="p">,</span> <span class="n">NUM_SERVOS</span><span class="o">&gt;</span> <span class="n">requested_state</span> <span class="o">=</span> <span class="p">{};</span>
<span class="p">};</span></code></pre>
</figure>

<p>The declaration for our Unit is nothing special. We store the PCA9685 interface, and store both the current state and requested state for each servo.</p>

<figure class="highlight">
  <pre><code class="language-cpp" data-lang="cpp"><span class="n">Update</span><span class="o">::</span><span class="n">Output</span> <span class="n">rpi_freenove_servo_driver</span><span class="o">::</span><span class="n">Update</span><span class="p">(</span><span class="k">const</span> <span class="n">Update</span><span class="o">::</span><span class="n">Input</span><span class="o">&amp;</span> <span class="n">input</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">const</span> <span class="kt">float</span> <span class="n">t</span> <span class="o">=</span> <span class="n">basis</span><span class="o">::</span><span class="n">core</span><span class="o">::</span><span class="n">MonotonicTime</span><span class="o">::</span><span class="n">Now</span><span class="p">().</span><span class="n">ToSeconds</span><span class="p">();</span>

  <span class="k">if</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">servo_0_request_degrees</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">requested_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">input</span><span class="p">.</span><span class="n">servo_0_request_degrees</span><span class="o">-&gt;</span><span class="n">value</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">if</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">servo_1_request_degrees</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">requested_state</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">input</span><span class="p">.</span><span class="n">servo_1_request_degrees</span><span class="o">-&gt;</span><span class="n">value</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">if</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">user_inputs</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">input</span><span class="p">.</span><span class="n">user_inputs</span><span class="o">-&gt;</span><span class="n">joysticks</span><span class="p">().</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
    <span class="c1">// If we have a joystick connected, use it</span>
    <span class="k">constexpr</span> <span class="kt">float</span> <span class="n">MAX_JOYSTICK_DEGREES_SEC</span> <span class="o">=</span> <span class="mf">180.0</span><span class="n">f</span><span class="p">;</span>
    <span class="c1">// Get the update rate for this handler</span>
    <span class="k">const</span> <span class="k">auto</span> <span class="n">duration</span> <span class="o">=</span> <span class="n">handlers</span><span class="p">[</span><span class="s">"Update"</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">rate_duration</span><span class="p">;</span> 

    <span class="c1">// TODO: move to config</span>
    <span class="k">constexpr</span> <span class="kt">size_t</span> <span class="n">AXIS_IDXES</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">2</span><span class="p">,</span> <span class="mi">5</span><span class="p">};</span>

    <span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">joystick</span> <span class="o">=</span> <span class="n">input</span><span class="p">.</span><span class="n">user_inputs</span><span class="o">-&gt;</span><span class="n">joysticks</span><span class="p">()[</span><span class="mi">0</span><span class="p">];</span>
    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">NUM_SERVOS</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">const</span> <span class="kt">float</span> <span class="n">delta</span> <span class="o">=</span> <span class="n">joystick</span><span class="p">.</span><span class="n">axes</span><span class="p">()[</span><span class="n">AXIS_IDXES</span><span class="p">[</span><span class="n">i</span><span class="p">]]</span> <span class="o">*</span> <span class="n">MAX_JOYSTICK_DEGREES_SEC</span> <span class="o">*</span> <span class="n">duration</span><span class="o">-&gt;</span><span class="n">ToSeconds</span><span class="p">();</span>
      <span class="n">requested_state</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">clamp</span><span class="p">(</span><span class="n">requested_state</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">-</span> <span class="n">delta</span><span class="p">,</span> <span class="o">-</span><span class="mf">70.0</span><span class="p">,</span> <span class="mf">70.0</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="p">{</span>
    <span class="c1">// Otherwise, rotate back and forth</span>
    <span class="c1">// this logic will eventually get moved out to a separate unit</span>
    <span class="n">requested_state</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">sin</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span> <span class="mf">2.0</span><span class="p">))</span> <span class="o">*</span> <span class="mf">70.0</span><span class="p">;</span>
    <span class="n">requested_state</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">sin</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span> <span class="mf">3.1</span><span class="p">))</span> <span class="o">*</span> <span class="mf">60.0</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="n">std</span><span class="o">::</span><span class="n">array</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">shared_ptr</span><span class="o">&lt;</span><span class="n">google</span><span class="o">::</span><span class="n">protobuf</span><span class="o">::</span><span class="n">DoubleValue</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">NUM_SERVOS</span><span class="o">&gt;</span> <span class="n">outputs</span><span class="p">;</span>
  <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">NUM_SERVOS</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// TODO: we will eventually implement smoothing here</span>
    <span class="n">current_state</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">requested_state</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="n">outputs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">make_shared</span><span class="o">&lt;</span><span class="n">google</span><span class="o">::</span><span class="n">protobuf</span><span class="o">::</span><span class="n">DoubleValue</span><span class="o">&gt;</span><span class="p">();</span>
    <span class="n">outputs</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">set_value</span><span class="p">(</span><span class="n">current_state</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
    <span class="kt">float</span> <span class="n">ms</span> <span class="o">=</span> <span class="n">DegressToPWMMS</span><span class="p">(</span><span class="n">current_state</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
  
    <span class="n">pca</span><span class="p">.</span><span class="n">set_pwm_ms</span><span class="p">(</span><span class="mi">8</span> <span class="o">+</span> <span class="n">i</span><span class="p">,</span> <span class="n">ms</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Magic - convert from our array output to our output type</span>
  <span class="k">return</span> <span class="n">std</span><span class="o">::</span><span class="n">apply</span><span class="p">([](</span><span class="k">auto</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">args</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">Update</span><span class="o">::</span><span class="n">Output</span><span class="p">{</span><span class="n">args</span><span class="p">...};</span> <span class="p">},</span> <span class="n">std</span><span class="o">::</span><span class="n">tuple_cat</span><span class="p">(</span><span class="n">outputs</span><span class="p">));</span>
<span class="p">}</span></code></pre>
</figure>

<p>Notice - we don’t have to store any messages, we don’t have to write any synchronizer code. Very straightforward.</p>

<h2 id="rpi_freenove_mecanum_driver">rpi_freenove_mecanum_driver</h2>

<figure class="highlight">
  <pre><code class="language-yaml" data-lang="yaml"><span class="na">args</span><span class="pi">:</span>
  <span class="na">i2c_device</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/dev/i2c-1"</span>
  <span class="na">address</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">int32_t</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">0x40</span>
<span class="na">threading_model</span><span class="pi">:</span>
  <span class="s">single</span>
<span class="na">cpp_includes</span><span class="pi">:</span> 
  <span class="pi">-</span> <span class="s">google/protobuf/wrappers.pb.h</span>
  <span class="pi">-</span> <span class="s">basis_robot_input.pb.h</span>
  <span class="pi">-</span> <span class="s">basis_robot_state.pb.h</span>

<span class="na">handlers</span><span class="pi">:</span>
  <span class="na">Update</span><span class="pi">:</span>
    <span class="na">sync</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">all</span>
      <span class="c1"># Run at 100fps</span>
      <span class="na">rate</span><span class="pi">:</span> <span class="m">0.01</span>
    
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">/user_inputs</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:basis::robot::input::InputState</span>
        <span class="na">optional</span><span class="pi">:</span> <span class="s">True</span>
        <span class="na">cached</span><span class="pi">:</span> <span class="s">True</span>

    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">/motor_state</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:basis::robot::state::MotorState</span></code></pre>
</figure>

<p>Same story as <code class="language-plaintext highlighter-rouge">rpi_freenove_servo_driver</code>. The only code of note is this:</p>

<figure class="highlight">
  <pre><code class="language-cpp" data-lang="cpp"><span class="n">std</span><span class="o">::</span><span class="n">array</span><span class="o">&lt;</span><span class="kt">float</span><span class="p">,</span> <span class="mi">4</span><span class="o">&gt;</span> <span class="n">XYTtoWheels</span><span class="p">(</span><span class="kt">float</span> <span class="n">x</span><span class="p">,</span> <span class="kt">float</span> <span class="n">y</span><span class="p">,</span> <span class="kt">float</span> <span class="n">theta</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">constexpr</span> <span class="kt">float</span> <span class="n">MAX_SPEED</span> <span class="o">=</span> <span class="mf">10.0</span><span class="n">f</span><span class="p">;</span> <span class="c1">// TODO: units</span>
  <span class="c1">// simple enough?</span>
  <span class="c1">// https://robotics.stackexchange.com/questions/20088/how-to-drive-mecanum-wheels-robot-code-or-algorithm</span>
  <span class="c1">// This isn't quite what we want, I think the /3.0 is bad</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="o">-</span><span class="n">MAX_SPEED</span> <span class="o">*</span> <span class="p">(</span><span class="n">y</span><span class="o">+</span><span class="n">x</span><span class="o">-</span><span class="n">theta</span><span class="p">),</span>
    <span class="n">MAX_SPEED</span> <span class="o">*</span> <span class="p">(</span><span class="n">y</span><span class="o">-</span><span class="n">x</span><span class="o">-</span><span class="n">theta</span><span class="p">),</span> <span class="c1">// Note: the hardware for this device has one reversed motor</span>
    <span class="o">-</span><span class="n">MAX_SPEED</span> <span class="o">*</span> <span class="p">(</span><span class="n">y</span><span class="o">+</span><span class="n">x</span><span class="o">+</span><span class="n">theta</span><span class="p">),</span>
    <span class="o">-</span><span class="n">MAX_SPEED</span> <span class="o">*</span> <span class="p">(</span><span class="n">y</span><span class="o">-</span><span class="n">x</span><span class="o">+</span><span class="n">theta</span><span class="p">),</span>
  <span class="p">};</span>
<span class="p">}</span></code></pre>
</figure>

<p>Mecanum wheel control is really simple. This function takes in the x/y joystick input and the sum of the triggers (theta), and outputs the power to each motor to satisfy those inputs.</p>

<h1 id="final-thoughts">Final thoughts</h1>

<p>This was pretty simple to do - helped of course by the availability of other libraries out there. I’m looking forward to getting LiDAR working - and then either SLAM (with an IMU?) or a simple planning+controls stack.</p>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[I’ve spent the past few weeks working on a small robot to both be able to give demos with and exercise our code. This post is about the current software architecture for the robot.]]></summary></entry><entry><title type="html">Building a Robot on Basis 01 - Hardware</title><link href="basisrobotics.tech/2024/11/22/basis-robot-01-hardware/" rel="alternate" type="text/html" title="Building a Robot on Basis 01 - Hardware" /><published>2024-11-22T00:00:00+00:00</published><updated>2024-11-22T00:00:00+00:00</updated><id>basisrobotics.tech/2024/11/22/basis-robot-01-hardware</id><content type="html" xml:base="basisrobotics.tech/2024/11/22/basis-robot-01-hardware/"><![CDATA[<p>I’ve spent the past few weeks working on a small robot to both be able to give demos with and exercise our code. This is a quick post on the hardware I bought, and what’s worked/not worked.</p>

<ul>
  <li><a href="/2024/11/22/basis-robot-01-hardware/">Part 01 - Hardware</a> (You’re here!)</li>
  <li><a href="/2024/11/24/basis-robot-02-software/">Part 02 - Software</a></li>
  <li>Part 03 - tf2 support and LiDAR</li>
</ul>

<h1 id="the-hardware">The hardware:</h1>
<ul>
  <li><a href="https://www.amazon.com/Freenove-Raspberry-Tracking-Avoidance-Ultrasonic/dp/B0CHJBY5HJ?th=1">FreeNove 4WD Smart Car Kit w/ Mecanum Wheels</a> - this is the main body and hardware for the robot</li>
  <li>Raspberry Pi 5 8GB</li>
  <li>Raspberry Pi 5 Active Cooler</li>
  <li>Raspberry Pi 5 SSD Hat</li>
  <li><a href="https://www.corsair.com/us/en/p/data-storage/cssd-f1000gbmp600mcr/mp600-micro-1tb-pcie-4-0-gen4-x4-nvme-m-2-2242-ssd-cssd-f1000gbmp600mcr">Corsair MP600 CORE MICRO 1TB NVMe SSD</a></li>
  <li><a href="https://www.bravoelectro.com/rsp-75-7-5.html?fbclid=IwY2xjawGt6KJleHRuA2FlbQIxMAABHZ49kYBXAzSuJn-doDtl-QpKzAqPjlhREPNqJcRPok3YdpcEtHOpsp-HCg_aem_lMoxt5n9ShF6aECgt2WBqA">Mean Well RSP-75-7.5</a></li>
  <li>Various other electrical bits and pieces</li>
  <li>SLAMTEC RPLIDAR (Future)</li>
</ul>

<p><img src="/assets/images/robot-hardware/assembled.jpg" alt="The assembled robot" width="500" /></p>

<p>All in all, I think I’ve spent a bit over $500 dollars on this project.</p>

<h2 id="freenove-4wd-smart-car-kit-w-mecanum-wheels">FreeNove 4WD Smart Car Kit w/ Mecanum Wheels</h2>

<p><img src="/assets/images/robot-hardware/dissassembled.jpg" alt="The kit, starting off" width="500" /></p>

<p>The good:</p>
<ul>
  <li>It all mostly worked</li>
  <li>Support was responsive after I burned out a servo</li>
  <li>Mecanum wheels are really cool, and the algo to drive them is very simple</li>
  <li>It didn’t blow up when I hooked in power backwards</li>
  <li>It uses standard hardware with good driver support (standard raspberry pi camera, PCA9685 for servos/wheels)</li>
</ul>

<p>The bad:</p>
<ul>
  <li>The test code doesn’t actually run on rpi-5 by default, you have to comment out a broken import (I should make a PR fixing this)</li>
  <li><a href="https://github.com/Freenove/Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi/blob/master/Code/Server-pi5/Motor.py#L46">The front left motor is wired backwards</a> - this isn’t fatal, more on this in the software post</li>
  <li>It’s not compatible with cooler and/or SSD hat by default - this is fixable, but annoying</li>
  <li>The camera servo mount is a little wobbly</li>
  <li>The camera cable is very flimsy, I nicked it and had to buy a new one</li>
  <li>There’s no way to run off of wall power, you have to buy batteries separately if you buy off of Amazon, and the batteries are hard to get out with damaging. I fixed this with some soldering.</li>
  <li>The nuts and bolts have a tendency to work themselves loose - I may go and reassemble it later with loctite blue on the threads</li>
  <li>I’ve already burnt out a servo - I don’t think it was my fault, but I’m not sure why this happened.</li>
</ul>

<p>Would I recommend this kit? Yes! This is the real life robotics experience and matches my professional experience. I’m not a hardware guy, but just being a bit handy I was able to assemble it without much difficulty.</p>

<h2 id="raspberry-pi-5-hat-woes">Raspberry Pi 5, HAT woes</h2>

<p>The pros:</p>
<ul>
  <li>It’s a Pi!</li>
  <li>Driver support is good</li>
</ul>

<p>The cons:</p>
<ul>
  <li>The wifi on this thing is horrendously bad (this might be fixable with some Linux magic)</li>
  <li>The CSI cable connector tends to come loose (this might not be the Pi’s fault, “loose camera cable” is a common robotics woe)</li>
</ul>

<p>I actually bought and assembled the Pi first. As I was intending on doing development directly on device, I went and got the cooler and SSD. Probably worth the money!</p>

<p>Overclocking the thing tended to result in instability. I will likely try and set up distcc or a cross-compilation workflow in the future to build off-device to speed things up - but compilation time isn’t bad by any means for a codebase of this size.</p>

<p>Integrating with the FreeNove board was…difficult.</p>

<p><img src="/assets/images/robot-hardware/wontfit.jpg" alt="Houston, we have a problem" width="500" /></p>

<p>If you don’t have any HATs, this is likely super easy. If you do, it’s really sad - both the cooler and the SSD HAT block things. There are third party SSD boards that don’t sit on top - if you’re going to use this setup, it might be easier to get one of those, and not use active cooling.</p>

<p>If you do want to use this setup, here’s how I fixed it.</p>

<p>My solution:</p>
<ul>
  <li>Get a <a href="https://www.amazon.com/Connectors-Raspberry-40-pin-Expansion-RAS-GP02/dp/B07MCW4KCM?source=ps-sl-shoppingads-lpcontext&amp;ref_=fplfs&amp;psc=1&amp;smid=ATVPDKIKX0DER">1 to 2 GPIO expansion</a>, use it instead of the extender that comes with the SSD HAT</li>
  <li>Run a cable in between the expansion and the FreeNove board</li>
</ul>

<p>I bought a cable that was too big and trimmed it down on the robot side, and bent pins on the expansion side, protected with electical tape. <strong>Do not do this</strong>. Go and find the proper sized cable (one with a 2x4 Pin DuPont connector on each end looks correct).</p>

<p><img src="/assets/images/robot-hardware/not_as_i_say.jpg" alt="Don't do this" width="500" /></p>

<h2 id="power">Power</h2>

<p>The next issue was power - with only running the Pi on batteries, I got maybe a bit under an hour’s worth of power. Not great, not bad - but even if replacing the batteries wasn’t so difficult, if I’m heads down I don’t want to stop and kill my docker container, workflow, etc. After doing some research (asking <a href="https://www.linkedin.com/in/ericyom/">EY</a>), I settled on a Mean Well power supply. I need ~8V and &gt;= 10A (supposedly), which put a lot of restrictions on using more consumer focused power supplies.</p>

<p><img src="/assets/images/robot-hardware/mean_well.jpg" alt="Mean, well" width="500" /></p>

<p>The Mean Well works great. The exposed mains power on top doesn’t make me too happy, but nothing some electrical tape can’t fix in the short term (and a cover in the long term). I bought a standard appliance cord to connect the wall power, and some 18/2 power cable for the DC side.</p>

<p>Some lessons:</p>
<ul>
  <li>Soldering XT30 connectors is harder that it seemed - I ended up ditching them for cheaper crimping connectors. Skill issue, likely.</li>
  <li>If you’re soldering directly onto some materials, it helps to sand them.</li>
</ul>

<p><img src="/assets/images/robot-hardware/battery_solder.jpg" alt="The kit, starting off" width="500" /></p>

<p>I’m likely going to pay someone to redo this for me in the future - what I have will work, it’s just not pretty. I’d love for a solution that lets the batteries sit in the holder and charge while also running the robot, but that’s beyond my expertise at the moment.</p>

<h2 id="lidar">LiDAR</h2>

<p>I haven’t yet hooked the LiDAR in, just ran it off of USB on my PC. It looks good. I’ll probably run it off of USB in the short term on the robot, but I’ll eventually properly hook up to the GPIO ports directly. The price is reasonable enough, but it is only a 2D LIDAR. I think in the future I want to a <a href="https://shop.unitree.com/collections/education-industry/products/unitree-4d-lidar-l1?variant=44806258589929">Unitree L1 LiDAR</a>, but I’m a little nervous about power and compute requirements.</p>

<p><img src="/assets/images/robot-hardware/lidar_scan.png" alt="lidar, soon" width="500" /></p>

<h1 id="final-thoughts">Final thoughts</h1>

<p>Hardware is both easier and tougher than I expected. As it turns out, <a href="https://x.com/shaiyanhkhan/status/1754197898814689379">You can just do things</a>. Any time I ran into an issue, it was mostly just a trip to Ace Hardware to fix it.</p>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[I’ve spent the past few weeks working on a small robot to both be able to give demos with and exercise our code. This is a quick post on the hardware I bought, and what’s worked/not worked.]]></summary></entry><entry><title type="html">(Fixing) Determinism in Robotics Testing</title><link href="basisrobotics.tech/2024/09/02/determinism/" rel="alternate" type="text/html" title="(Fixing) Determinism in Robotics Testing" /><published>2024-09-02T00:00:00+00:00</published><updated>2024-09-02T00:00:00+00:00</updated><id>basisrobotics.tech/2024/09/02/determinism</id><content type="html" xml:base="basisrobotics.tech/2024/09/02/determinism/"><![CDATA[<p>Here at Basis, we’re building a production/testing focused robotics framework. Along those lines, determinism when testing is one of our primary goals. This article is focused on how basis can not only achieve determinism in tests, but use it to run your tests lightning fast.</p>

<p>Here’s a preview:</p>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">replay</code></th>
      <th><code class="language-plaintext highlighter-rouge">deterministic_replay</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/assets/images/slow.gif" alt="slow" /></td>
      <td><img src="/assets/images/fast.gif" alt="fast" /></td>
    </tr>
  </tbody>
</table>

<h2 id="background">Background</h2>

<p>Determinism in robotics is an ongoing problem, both for runtime and for testing. This post aims to show a solution for testing/simulation.</p>

<p>The big issues come down to this:</p>
<ul>
  <li>ROS (and other robotics frameworks) don’t have any way of running deterministically, even in testing mode.</li>
  <li>Code running at different speeds between the robot and your development workstation is a problem</li>
  <li>Code running at a different speed in CI and on the robot is a <em>huge</em> problem, made worse by either cheapening out on CI hardware or running with too much parallelism in an effort to speed up test times.</li>
  <li>At a low level, running the same code on the same system will behave differently due to transport layer and scheduling nondeterminism.</li>
</ul>

<p>As a result of the above, making integration tests for robots sucks, and the tooling that does exist kinda sucks as well. An integration test running in CI might look like this:</p>
<ol>
  <li>Launch some process orchestrator (ROS Master, etc)</li>
  <li>Launch some subset of your robot with a custom launch file
    <ul>
      <li>if you’re lucky, someone’s added a test flag to your main launch file</li>
      <li>if you’re unlucky, there’s a separate launch file for tests that might be out of date to what your robot actually runs</li>
    </ul>
  </li>
  <li>Replay some recorded data at a slower than realtime speed to try and dodge transport/scheduling nondeterminism</li>
  <li>Wait extra long because you’re running at slower than realtime speed.</li>
  <li>Your test fails CI.</li>
  <li>Cross fingers that the cleanup for the test actually kills all the processes the test launched.</li>
  <li>Go repeat the above on your development desktop.</li>
  <li>The test succeeds (or maybe randomnly doesn’t succeed).</li>
  <li>Rerun the test in CI, waiting even longer.</li>
  <li>The test succeeds.</li>
  <li>Throw your hands up in the air, give up, and merge your code, blaming the simulation team if it fails again in <code class="language-plaintext highlighter-rouge">main</code>.</li>
</ol>

<p>In the end, this just trains the engineering team to ignore test results, and discourages creating larger/more complex tests and simulations. When a test fails, is it due to the framework or due to the actual robotics code? Especially in safety critical environments, it’s important to rule out flakes.</p>

<p>Basis is aiming for tests to give the same result, every time. Other types of determinism will also come later (code running the same time on replay as it did at runtime, for example).</p>

<h3 id="sources-of-nondeterminism">Sources of nondeterminism</h3>

<p>Nondeterminism can come from a wide variety of sources.</p>

<p>The current main focus of efforts creating basis (and the focus of this blog post) is nondeterminism from the transport layer and scheduling related nondeterminism.</p>

<p>Nondeterminism at the transport layer includes nondeterminism in the network stack, the order sockets are processed in, what happens when two messages come in at the same moment, that sort of thing.</p>

<p>Scheduling nondeterminism encompasses threading related woes, performance differences between test executions, etc.</p>

<p>A non-exhaustive list of other sources of nondeterminism:</p>
<ul>
  <li>calls to random()</li>
  <li>use of the system clock</li>
  <li>use of networked resources</li>
  <li>data races</li>
  <li>compiler flags</li>
  <li>cosmic rays</li>
  <li>pointer comparisons</li>
</ul>

<p>(I’ve seen all of these in test environements before.)</p>

<h3 id="how-can-we-fix-this">How can we fix this?</h3>

<p>A few rules:</p>
<ol>
  <li>All code to be run deterministically happens in response to a message or a timer (in basis terms, everything lives inside Handlers)</li>
  <li>No side channel communication between Handlers. Use a topic - Basis supports runtime only topics (raw C++ structs), so use them.
    <ul>
      <li>This isn’t strictly true - Handlers in the same Unit will share a C++ class, setting a variable is a side channel. This is mostly an issue with complex Units that allow parallel execution of Handlers. We will (eventually) provide tools to help with this case.</li>
    </ul>
  </li>
  <li>(Along with 2) Robotics code doesn’t know about what a Subscriber or Publisher are. They take messages on topics as inputs and output messages on topics as outputs.
    <ul>
      <li>This doesn’t stop tooling that does care about these concepts, it just means those tools can’t be run deterministically as part of a test.</li>
      <li>This does mean that externally triggered code (sensor drivers) won’t be determinisitic, but that’s not a problem for most tests.</li>
    </ul>
  </li>
  <li>All Handlers have associated metadata describing their inputs, outputs, execution conditions.</li>
  <li>All Units (think: ROS Node) can be loaded dynamically and contain the metadata</li>
  <li>Code inside Handlers is determinisitic (duh).</li>
</ol>

<p>From these rules, one can build a scheduler that looks at the requested Units to be run, looks at the contents of any data to be replayed, and appropriately invokes each Handler in the correct (and deterministic) order, with the correct data. Basis is built from the ground up to support this use case.</p>

<h2 id="demonstration">Demonstration</h2>

<p>Let’s show how basis can help with determinism. First we’ll record some data from a live run. Then we’ll run a <code class="language-plaintext highlighter-rouge">replay</code> test on that data and find issues with the testing process. Finally, we’ll show <code class="language-plaintext highlighter-rouge">deterministic_replay</code> at work, fixing those issues.</p>

<h3 id="recording-some-data">Recording some data</h3>

<p>Here’s an example of a basis Unit that wants to do some work. This Unit will take in a single message, block for 100ms (doing fake “work”) and then exit, allowing other callbacks to run.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">threading_model</span><span class="pi">:</span>
  <span class="s">single</span>
<span class="na">cpp_includes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">simple_pub_sub.pb.h</span>
<span class="na">handlers</span><span class="pi">:</span>
  <span class="na">OnChatter</span><span class="pi">:</span>
    <span class="na">sync</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">all</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">/chatter</span><span class="pi">:</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">protobuf:StringMessage</span>
</code></pre></div></div>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">OnChatter</span><span class="o">::</span><span class="n">Output</span> <span class="n">simple_sub</span><span class="o">::</span><span class="n">OnChatter</span><span class="p">(</span><span class="k">const</span> <span class="n">OnChatter</span><span class="o">::</span><span class="n">Input</span> <span class="o">&amp;</span><span class="n">input</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Convert protobuf nanoseconds into a basis timestamp</span>
  <span class="k">auto</span> <span class="n">send_stamp</span> <span class="o">=</span> <span class="n">basis</span><span class="o">::</span><span class="n">core</span><span class="o">::</span><span class="n">MonotonicTime</span><span class="o">::</span><span class="n">FromNanoseconds</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="n">chatter</span><span class="o">-&gt;</span><span class="n">send_stamp</span><span class="p">());</span>
  <span class="n">BASIS_LOG_INFO</span><span class="p">(</span><span class="s">"OnChatter: {} {}"</span><span class="p">,</span> <span class="n">input</span><span class="p">.</span><span class="n">chatter</span><span class="o">-&gt;</span><span class="n">message</span><span class="p">(),</span> <span class="n">send_stamp</span><span class="p">.</span><span class="n">ToSeconds</span><span class="p">());</span>

  <span class="c1">// Calculate delay between "now" and when the message was sent</span>
  <span class="n">basis</span><span class="o">::</span><span class="n">core</span><span class="o">::</span><span class="n">Duration</span> <span class="n">delay</span> <span class="o">=</span> <span class="n">input</span><span class="p">.</span><span class="n">time</span> <span class="o">-</span> <span class="n">send_stamp</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">delay</span> <span class="o">&gt;</span> <span class="n">basis</span><span class="o">::</span><span class="n">core</span><span class="o">::</span><span class="n">Duration</span><span class="o">::</span><span class="n">FromSeconds</span><span class="p">(</span><span class="mf">0.2</span><span class="p">))</span> <span class="p">{</span>
    <span class="n">BASIS_LOG_WARN</span><span class="p">(</span><span class="s">"/chatter delayed by {:.2f}s - queueing has occured"</span><span class="p">,</span> <span class="n">delay</span><span class="p">.</span><span class="n">ToSeconds</span><span class="p">());</span>
  <span class="p">}</span>
  <span class="k">constexpr</span> <span class="kt">int</span> <span class="n">work_time_ms</span> <span class="o">=</span> <span class="mi">2000</span><span class="p">;</span>
  <span class="n">BASIS_LOG_INFO</span><span class="p">(</span><span class="s">"Doing {} ms worth of work"</span><span class="p">,</span> <span class="n">work_time_ms</span><span class="p">);</span>
  <span class="n">std</span><span class="o">::</span><span class="n">this_thread</span><span class="o">::</span><span class="n">sleep_for</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">milliseconds</span><span class="p">(</span><span class="n">work_time_ms</span><span class="p">));</span>

  <span class="k">return</span> <span class="n">OnChatter</span><span class="o">::</span><span class="n">Output</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Running this unit along with another unit to produce on <code class="language-plaintext highlighter-rouge">/chatter</code> at 1Hz will give a console output resembling this</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">124998.122650826</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Recording</span> <span class="p">(</span><span class="n">async</span><span class="p">)</span> <span class="n">to</span> <span class="o">/</span><span class="n">tmp</span><span class="o">/</span><span class="n">demo_124998</span><span class="mf">.122377076</span><span class="p">.</span><span class="n">mcap</span>
<span class="p">[</span><span class="mf">124998.126956742</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Running</span> <span class="n">process</span> <span class="n">with</span> <span class="mi">2</span> <span class="n">units</span>
<span class="p">[</span><span class="mf">124998.130765492</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="p">[</span><span class="mf">124998.134928909</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">simple_pub</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="p">[</span><span class="mf">124999.137999076</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_pub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">PublishAt1Hz</span>
<span class="p">[</span><span class="mf">124999.138074076</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">124999.138065618</span>
<span class="p">[</span><span class="mf">124999.138080410</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125000.136988785</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_pub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">PublishAt1Hz</span>
<span class="p">[</span><span class="mf">125000.137143077</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125000.137124493</span>
<span class="p">[</span><span class="mf">125000.137151452</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125001.136178202</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_pub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">PublishAt1Hz</span>
<span class="p">[</span><span class="mf">125001.136319869</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125001.136305744</span>
<span class="p">[</span><span class="mf">125001.136327452</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125002.137870703</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_pub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">PublishAt1Hz</span>
<span class="p">[</span><span class="mf">125002.138054536</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125002.137991619</span>
<span class="p">...</span>
</code></pre></div></div>

<h3 id="replaying-the-data">Replaying the data</h3>

<p>Great, but now let’s pretend this was data recorded on a robot. We might want to run <code class="language-plaintext highlighter-rouge">replay /tmp/demo_124998.122377076.mcap</code>, along with <code class="language-plaintext highlighter-rouge">simple_sub</code>. If the hardware is similar between the two environments, we might see something like this:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">125458.039238910</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Running</span> <span class="n">process</span> <span class="n">with</span> <span class="mi">1</span> <span class="n">units</span>
<span class="p">[</span><span class="mf">125458.040486243</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="p">[</span><span class="mf">124998.307061284</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">simple_sub</span> <span class="n">detected</span> <span class="n">playback</span> <span class="n">restart</span><span class="p">,</span> <span class="n">restarting</span><span class="p">...</span>
</code></pre></div></div>
<p>(note the time jump here, we got a new simulated time step)</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">124998.977061284</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Running</span> <span class="n">process</span> <span class="n">with</span> <span class="mi">1</span> <span class="n">units</span>
<span class="p">[</span><span class="mf">124998.977061284</span><span class="p">]</span> <span class="p">[</span><span class="n">launch</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Started</span> <span class="kr">thread</span> <span class="n">with</span> <span class="n">unit</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="p">[</span><span class="mf">125001.137061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125001.137061284</span>
<span class="p">[</span><span class="mf">125001.137061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125002.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125002.147061284</span>
<span class="p">[</span><span class="mf">125002.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125003.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125003.147061284</span>
<span class="p">[</span><span class="mf">125003.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">100</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">...</span>
</code></pre></div></div>

<h3 id="problem-1---missing-data-nondeterminism">Problem 1 - missing data, nondeterminism.</h3>

<p>Rerunning this a few times, we see Problem #1 - we sometimes miss the first message of the test, and get an output:</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">125002.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125002.147061284</span>
</code></pre></div></div>

<p>A quick check with <code class="language-plaintext highlighter-rouge">mcap-cli</code> reveals…</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basis</span><span class="err">@</span><span class="n">aee413836118</span><span class="o">:/</span><span class="n">basis</span><span class="o">/</span><span class="n">demos</span><span class="o">/</span><span class="n">simple_pub_sub</span><span class="o">/</span><span class="n">build</span><span class="err">$</span> <span class="o">~/</span><span class="n">mcap</span><span class="o">-</span><span class="n">linux</span><span class="o">-</span><span class="n">arm64</span> <span class="n">cat</span> <span class="o">--</span><span class="n">json</span> <span class="o">/</span><span class="n">tmp</span><span class="o">/</span><span class="n">demo_124998</span><span class="mf">.122377076</span><span class="p">.</span><span class="n">mcap</span> <span class="o">--</span><span class="n">topics</span> <span class="o">/</span><span class="n">chatter</span>
<span class="p">{</span><span class="s">"topic"</span><span class="o">:</span><span class="s">"/chatter"</span><span class="p">,</span><span class="s">"sequence"</span><span class="o">:</span><span class="mi">0</span><span class="p">,</span><span class="s">"log_time"</span><span class="o">:</span><span class="mf">124999.138044993</span><span class="p">,</span><span class="s">"publish_time"</span><span class="o">:</span><span class="mf">124999.138044993</span><span class="p">,</span><span class="s">"data"</span><span class="o">:</span><span class="p">{</span><span class="s">"sendStamp"</span><span class="o">:</span><span class="s">"124999137959660"</span><span class="p">,</span> <span class="s">"message"</span><span class="o">:</span><span class="s">"Hello, world!"</span><span class="p">}}</span>
<span class="p">{</span><span class="s">"topic"</span><span class="o">:</span><span class="s">"/chatter"</span><span class="p">,</span><span class="s">"sequence"</span><span class="o">:</span><span class="mi">0</span><span class="p">,</span><span class="s">"log_time"</span><span class="o">:</span><span class="mf">125000.137092327</span><span class="p">,</span><span class="s">"publish_time"</span><span class="o">:</span><span class="mf">125000.137092327</span><span class="p">,</span><span class="s">"data"</span><span class="o">:</span><span class="p">{</span><span class="s">"sendStamp"</span><span class="o">:</span><span class="s">"125000136919993"</span><span class="p">,</span> <span class="s">"message"</span><span class="o">:</span><span class="s">"Hello, world!"</span><span class="p">}}</span>
<span class="p">{</span><span class="s">"topic"</span><span class="o">:</span><span class="s">"/chatter"</span><span class="p">,</span><span class="s">"sequence"</span><span class="o">:</span><span class="mi">0</span><span class="p">,</span><span class="s">"log_time"</span><span class="o">:</span><span class="mf">125001.136257244</span><span class="p">,</span><span class="s">"publish_time"</span><span class="o">:</span><span class="mf">125001.136257244</span><span class="p">,</span><span class="s">"data"</span><span class="o">:</span><span class="p">{</span><span class="s">"sendStamp"</span><span class="o">:</span><span class="s">"125001136105286"</span><span class="p">,</span> <span class="s">"message"</span><span class="o">:</span><span class="s">"Hello, world!"</span><span class="p">}}</span>
<span class="p">...</span>
</code></pre></div></div>
<p>Several messages are missing from the test. Even our most complete replay was missing <code class="language-plaintext highlighter-rouge">124999</code> and <code class="language-plaintext highlighter-rouge">125000</code>.</p>

<h3 id="problem-2---performance-differences-can-mean-nondeterminism">Problem 2 - performance differences can mean nondeterminism</h3>

<p>Now what if instead, our testing environment was slower (overloaded CI, different/no GPU, less cores). In this scenario, let’s pretend that the work takes 2000 ms to run, and set <code class="language-plaintext highlighter-rouge">constexpr int work_time_ms = 2000;</code>.</p>

<p>Let’s run that same replay test.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="mf">125002.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125002.137789828</span>
<span class="p">[</span><span class="mf">125002.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125004.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125003.137948037</span>
<span class="p">[</span><span class="mf">125004.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">warning</span><span class="p">]</span> <span class="o">/</span><span class="n">chatter</span> <span class="n">delayed</span> <span class="n">by</span> <span class="mx">1.01s</span> <span class="o">-</span> <span class="n">queueing</span> <span class="n">has</span> <span class="n">occured</span>
<span class="p">[</span><span class="mf">125004.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125006.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125004.13672012</span>
<span class="p">[</span><span class="mf">125006.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">warning</span><span class="p">]</span> <span class="o">/</span><span class="n">chatter</span> <span class="n">delayed</span> <span class="n">by</span> <span class="mx">2.01s</span> <span class="o">-</span> <span class="n">queueing</span> <span class="n">has</span> <span class="n">occured</span>
<span class="p">[</span><span class="mf">125006.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125008.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125005.136193412</span>
<span class="p">[</span><span class="mf">125008.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">warning</span><span class="p">]</span> <span class="o">/</span><span class="n">chatter</span> <span class="n">delayed</span> <span class="n">by</span> <span class="mx">3.01s</span> <span class="o">-</span> <span class="n">queueing</span> <span class="n">has</span> <span class="n">occured</span>
<span class="p">[</span><span class="mf">125008.147061284</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
</code></pre></div></div>

<p>Look at that - rather than the second message being processed at <code class="language-plaintext highlighter-rouge">125003.137948037</code>, it’s processed at <code class="language-plaintext highlighter-rouge">125004.147061284</code>, a second later than expected (or exactly as one would expect for adding an additional second of blocking…). The delay will only continue to go up, and the queues backing the pub/sub system generally won’t be configured for infinite space, resulting in dropped messages and incorrect timings.</p>

<p>If the original runtime looked like this:</p>

<p><img src="/assets/diagrams/runtime_msg.svg" alt="" /></p>

<p><em>(Apologies for the default LucidChart colorscheme)</em></p>

<p>With our artifical slowdown, it now looks like this:</p>

<p><img src="/assets/diagrams/slow_msg.svg" alt="" /></p>

<p>Note the queueing - we can’t process more messages while <code class="language-plaintext highlighter-rouge">OnChatter</code> is executing, but the replay system doesn’t know this and keeps publishing messages.</p>

<h3 id="problem-3---integration-tests-are-slow-clunky-and-dont-pair-well-with-unit-testing-frameworks">Problem 3 - integration tests are slow, clunky, and don’t pair well with unit testing frameworks</h3>

<p>If each message takes 100ms to process, and we replay 10 messages, how long would you expect this test to take? With a regular integration test: 10 seconds, for 1 second worth of work.</p>

<p>Let’s say our CI hardware is even faster, able to process a message in 10ms - the test still takes 10 seconds to run, for 100ms worth of work.</p>

<p>Along with this, how would we integrate this test with <code class="language-plaintext highlighter-rouge">gtest</code>? We’d have to build some sort of framework that manages forking or shelling out to launch each part of it, with timeouts, management for grandchildren processes, etc.</p>

<p>If we wanted to programmatically create messages (avoiding the use of serialized data on disk), or create a single message and check the output of the system after each step, how would we do it? With ROS you’d have two choices:</p>
<ol>
  <li>Stand up a ROS Master, launch the nodes you care about, initialize your test as a ROS node, publish the message, and spin until you (maybe) get a response. (Don’t forget to turn off paralellism for your tests!)</li>
  <li>Break apart your ROS Nodes into libraries, manually shuffling messages between the business logic for each node (good luck keeping this in sync with the launch files)</li>
</ol>

<h2 id="the-solution">The solution</h2>

<p>Let’s rerun this with basis’s <em>deterministic</em> replayer.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">deterministic_replay</span> <span class="o">/</span><span class="n">tmp</span><span class="o">/</span><span class="n">demo_124998</span><span class="mf">.122377076</span><span class="p">.</span><span class="n">mcap</span> <span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">demos</span><span class="o">/</span><span class="n">simple_pub_sub</span><span class="o">/</span><span class="n">launch_single_process</span><span class="p">.</span><span class="n">yaml</span> <span class="o">--</span><span class="n">disable_unit</span> <span class="n">demo</span><span class="o">:</span><span class="n">simple_pub</span>
<span class="p">[</span><span class="mf">129000.980036858</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Found</span> <span class="n">unit</span> <span class="o">/</span><span class="n">simple_sub</span> <span class="n">simple_sub</span> <span class="n">at</span> <span class="o">/</span><span class="n">opt</span><span class="o">/</span><span class="n">basis</span><span class="o">/</span><span class="n">unit</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">.</span><span class="n">unit</span><span class="p">.</span><span class="n">so</span>
<span class="p">[</span><span class="mf">129000.982984441</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Starting</span> <span class="n">deterministic</span> <span class="n">playback</span><span class="p">...</span>
<span class="p">[</span><span class="mf">129000.984189191</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">replaying</span> <span class="n">topic</span> <span class="o">/</span><span class="n">chatter</span>
<span class="p">[</span><span class="mf">129000.984319524</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">replaying</span> <span class="n">topic</span> <span class="o">/</span><span class="n">log</span>
<span class="p">[</span><span class="mf">129000.984376316</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Beginning</span> <span class="n">replay</span> <span class="n">at</span> <span class="mf">124998.127061284</span>
<span class="p">[</span><span class="mf">129000.985144524</span><span class="p">]</span> <span class="p">[</span><span class="n">replayer</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Initialized</span> <span class="n">unit</span> <span class="o">/</span><span class="n">simple_sub</span>
<span class="p">[</span><span class="mf">124999.238044993</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">124999.13795966</span>
<span class="p">[</span><span class="mf">124999.238044993</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125000.237092327</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125000.136919993</span>
<span class="p">[</span><span class="mf">125000.237092327</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">Doing</span> <span class="mi">2000</span> <span class="n">ms</span> <span class="n">worth</span> <span class="n">of</span> <span class="n">work</span>
<span class="p">[</span><span class="mf">125001.236257244</span><span class="p">]</span> <span class="p">[</span><span class="o">/</span><span class="n">simple_sub</span><span class="p">]</span> <span class="p">[</span><span class="n">info</span><span class="p">]</span> <span class="n">OnChatter</span><span class="o">:</span> <span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="o">!</span> <span class="mf">125001.136105286</span>
</code></pre></div></div>

<p>Beautiful - we even caught the first few messages that were dropped before. The scheduler now waits on Handlers that are expected to be complete (in terms of simulated time) but aren’t yet finished (in realtime).</p>

<p><img src="/assets/diagrams/fast_msg.svg" alt="" /></p>

<p>We can see that we don’t do any more work (such as replaying messages) while <code class="language-plaintext highlighter-rouge">OnChatter</code> is executing, as the Handler is only supposed to take 100ms of simulation time. It sucks that we’re running twice as slow as realtime, but it’s better than running with inaccurate messages.</p>

<p>Problem 1, Problem 2 - solved.</p>

<p>As for Problem 3… remember that preview gif from the beginning of the article?</p>

<p>Let’s turn the work time all the way down to 10ms. Because <code class="language-plaintext highlighter-rouge">deterministic_replay</code> knows when code should run, it also knows when code <em>isn’t</em> running. We can run the code as fast as our CPU will let us, ignoring the fact that the replay data publishes at 1Hz. This enables integration tests that run lightning fast.</p>

<p>(Other data replay systems usually have some form of rate multiplier command - but this is tough to tune for all conditions).</p>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">replay</code></th>
      <th><code class="language-plaintext highlighter-rouge">deterministic_replay</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/assets/images/slow.gif" alt="slow" /></td>
      <td><img src="/assets/images/fast.gif" alt="fast" /></td>
    </tr>
  </tbody>
</table>

<p>Everything can be run in a single process for test mode, meaning a coordinator (master) process is only needed if communication with visualizers or other tooling is required (not required for unit testing). One could link <code class="language-plaintext highlighter-rouge">deterministic_replay</code> into a unit test, then run a launch file (or even a programatically created list of Units), push in messages, query outputted messages and inner states of nodes, modify in flight messages, etc. No extra processes needed.</p>

<h2 id="even-further">Even further</h2>

<p>With this power, we can do more:</p>
<ul>
  <li>Find out what happens if one of our messages arrives later than expected, or test different timing related error paths. How well have you actually tested your degraded states around timing?</li>
  <li>The default behavior of <code class="language-plaintext highlighter-rouge">deterministic_replay</code> is to always rerun events that happen at the same time in the same order. What if instead we reversed it? Or randomized it with a seed? With message send time jitter randomization added on top, one could long tail test for timing related bugs.</li>
  <li>Test out “what if” situations that may be expensive to implement. Let’s say your Perception Lead says he can speed up the perception stack by 15% with a quarter’s worth of work and two engineers. It sounds good on paper, but will a faster perception stack actually lead to better robot performance? Before doing the quarter’s worth of work, run your integration test suite with the promised timings, instead.</li>
  <li><code class="language-plaintext highlighter-rouge">lldb -- deterministic_replayer ...</code> just works, and can pause all units, properly, without having to mess with fork modes.</li>
</ul>

<h3 id="hasnt-this-been-built-yet">Hasn’t this been built yet?</h3>

<p>This sort of tooling has been asked for before:</p>
<ul>
  <li><a href="https://answers.ros.org/question/254354/deterministic-replay-and-debugging/">Unanswered answers.ros.org post</a></li>
  <li><a href="http://wiki.ros.org/ROS/Tutorials/Recording%20and%20playing%20back%20data#rosbag.2FTutorials.2FRecording_and_playing_back_data.The_limitations_of_rosbag_record.2Fplay">ROS1’s documentation: “For nodes like turtlesim, where minor timing changes in when command messages are processed can subtly alter behavior, the user should not expect perfectly mimicked behavior.”</a></li>
  <li>Various other ROS answers posts</li>
  <li>Engineers internal to companies integrating with Gazebo/Applied Intuition/internal tools/etc.</li>
</ul>

<p>Various groups have tried approaching it:</p>
<ul>
  <li><a href="https://roscon.ros.org/2017/presentations/ROSCon%202017%20Determinism%20in%20ROS.pdf">A cool deck on ROS runtime determinism from BOSCH</a> (runtime determinism isn’t covered here, but basis does make this easier)</li>
  <li><a href="https://pdfs.semanticscholar.org/fc61/d8c5cfa2b34d9d7716618d6c76892db5ea83.pdf">Flow framework</a> <a href="https://github.com/ZebraDevs/flow_ros?tab=readme-ov-file">https://github.com/ZebraDevs/flow_ros?tab=readme-ov-file</a> (This appears to be mostly a intermediary between ROS publishers and robotics business logic - I approve, good architecture. But where’s the deterministic replay?)</li>
  <li><a href="https://github.com/uulm-mrm/ros2_def">ROS2 DEF</a> - A bolt on system for ROS2 from Ulm University. A valiant effort, but abandoned and incomplete. Can’t fully work due to ROS2 architecture.</li>
  <li>Various large robotics companies having to implement this internally to various levels of completeness, using various strategies. Unfortunately these are going to be very company specific and they aren’t likely to publish or sell them.</li>
</ul>

<p>Funnily enough, Applied Intuition completely sidesteps the issue, <a href="https://www.appliedintuition.com/blog/why-determinism-matters-for-successful-adas-and-ad-development">saying that determinism in ADAS is good, but not giving any answers for how to achieve it when integrating.</a></p>]]></content><author><name>Kyle Franz</name></author><summary type="html"><![CDATA[Here at Basis, we’re building a production/testing focused robotics framework. Along those lines, determinism when testing is one of our primary goals. This article is focused on how basis can not only achieve determinism in tests, but use it to run your tests lightning fast.]]></summary></entry></feed>