The "proper" solution would be for apps to support 64-bit, at which point they can just run natively on the device. The whole reason Tango is required is that there is still a large number of apps that use 32-bit native libraries. This is particularly true in markets such as China which don't use the Google Play Store.
When ARM released AArch64, it was a completely new instruction set rather than an extension of the existing 32-bit ISA (like x86-64). AArch64 is a much better designed ISA than AArch32 and supporting both in hardware effectively doubles the complexity of the instruction decoder, so it was clear that eventually there would be AArch64-only CPUs (as there are today).
The technology behind Tango started as a research project while I was doing my PhD. After finishing my dissertation in 2016, I looked into opportunities to commercialize it by contacting every company that was building or planning to build an AArch64-only CPU. There are sufficiently few that it is easily doable even for a small company.
Building a production-ready binary translator is technically challenging and requires a lot of work. The difficult parts are achieving high performance (Tango scores within 10% of native 32-bit execution on benchmarks), low latency (using AOT translation to accelerate startup times) and compatibility (Tango was tested against the top 1000 Android apps and works with all of them).
Already by 2017, Tango was capable of translating AArch32 Android applications. At that point it makes more sense for companies to license our technology rather than developing their own implementation from scratch.
The first commit in what would eventually become Tango was all the way back in 2014, so this project has been in development for a long time. As such there have been many technical challenges.
One challenge that particularly comes to mind was dealing with anti-emulating/anti-debugging code in various Android applications. These apps would do all sorts of crazy things like attaching to themselves with ptrace, installing bizarre seccomp filters which check for specific 32-bit syscalls and using self-modifying code without proper cache flushing to check for the presence of an instruction cache.
The solution for each of those was to emulate the relevant functionality well enough to trick these apps into thinking they were running natively. Although in the case of self-modifying there was no good solution and we ended up hard-coding some particular instruction sequences in the translator for special handling.
One thing that really made the above possible is that for Tango v2.0 we re-wrote a large part (~half) of the codebase in Rust, which was previously entirely written in C. In particular, the ptrace emulation code needs to maintain a lot of internal state about traced threads. This requires maintaining complex data structures, and the ability to easily use enums, Option, HashMap, etc, is a huge help for this.
The main downside of seqlocks is that you usually can't have pointers in them. Since the basic scheme is read, do stuff, check if sequence counter changed, you need to make sure that you don't dereference a potentially invalid pointer in the "do stuff" phase if another thread is concurrently modifying the data.
RCU avoids this issue by essentially delaying freeing of objects until there are no longer any threads accessing it. But it requires very complicated mechanisms to track all active threads, whereas a seqlock is a very simple and self-contained.
In both cases the read-side overhead is tiny: for example on x86, you don't need any memory barrier or atomic instructions.