Designing package:intl4x
#1039
mosuem
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
tl;dr
Replacing a Dart package by a Rust package using FFI and hooks for more features and reduced maintenance cost.
How it started
Basics
Internationalization (i18n) and localization (l10n) are at the heart of accessibility and success for many apps. Only ~20% of the worlds population speak English and only ~5% are native speakers. Most countries have their own languages, often multiple ones, with their own rules on how to display numbers, dates, how to sort lists of words etc.
All this made it clear that the Dart team must ship a library to handle this:
package:intl. It is one of the most used packages on pub.dev and can handle most operations needed from such a library.Problem
I18n is unfortunately quite complex, with data being updated twice a year, and many edge cases resulting in hard to write code. This results in a steady stream of bugs and missing features accumulating over time. All this results in a poor experience and high maintenance costs, exacerbated by the pre-Dart 1.0 code in
package:intl.Solution Options
1. Keep updating
package:intl🔴This has the obvious drawbacks of continuing the existing problems, but the advantage of not starting from zero and a clear path forwards.
2. Write a new Dart library 🔴
Very high initial costs and some ongoing maintenance in the future but being able to do nice green field development with the associated freedoms in design.
3. Re-use existing libraries 🟢
There are many libraries for i18n, such as ICU4J (Java), ICU4C (C), or the new ICU4X (Rust). These are built and maintained by i18n teams and it would be great to be able to reuse their efforts to bring down maintenance.
Being a small team with many responsibilities, and wanting to use the i18n teams' expertise, we went with option 3.
Challenges
To reuse an existing package, we must solve some issues:
Solutions
We selected ICU4X as a new modular library optimized for code size, helping with 2.. This gave the challenge of having to bridge to a Rust library via FFI solved using the diplomat codegen tool.
To ship the compiled Rust code with the Dart code, we introduced build hooks as a new Dart feature. To not increase code size, we need to treeshake the native code, which is done in the new link hooks. And finally, we needed to write a layer around the ICU4X generated API to make it feel comfortable for Dart users, keeping it as thin as possible to reduce maintenance.
To further reduce code size on the web, the library uses the existing ECMA Intl object in the browser bringing down the code size to virtually zero.
All this should happen in the background, giving users no indication performance-wise that they are using a package not written in Dart.
Technical details
When a user wants to format a date in year month day format, in Dart they will type (ref)
this translates to an interface shared by native and web code (ref)
which is implemented by the native version as (ref)
. This goes to the diplomat auto-generated wrapper of the C API of ICU4X in package:icu4x (ref)
which directs to the autogenerated external method references linking directly to a Rust function (ref). This uses two special features from Dart,
@Nativeand@RecordUse.@Nativerelates the method to the symbol in a dynamic library via the build hooks. In this case, the function corresponds to a symbol calledicu4x_DateFormatter_create_ymd_mv1in a dynamic library attached in the build hook to the code.@RecordUsemeans that creations of this instance are tracked and can be retrieved during the link hook phase, which runs after kernel compilation. So we can retrieve a list of symbols used from the library and treeshake out all the unused symbols, bringing down the binary size from ~37MB to ~1.6MB, depending on the symbols used. The treeshaking works by compiling the Rust library to a static library in the build hook, and then running the link hook with a bunch of flags and the list of recorded symbols to obtain a small dynamic library ready to be shipped with the compiled Dart code.. The corresponding Rust code is then (ref)
Size comparisons
We are comparing two very simple programs. This is in no way representative of a real app - the
intlandintl4xpackages are hard to compare due to different feature sets.package:intl:package:intl4x:Web
package:intlresults inwhile
package:intl4xonly showswhich is only ¼ of the size.
Native
Here it is slightly different, for
package:intlwe getwhile
package:intl4xresults inwhich is slightly larger, due to the overhead of the ICU4X library from its larger feature set.
Beta Was this translation helpful? Give feedback.
All reactions