Sharing Code between iOS and Android with Rust

Sharing Code between iOS and Android with Rust

When having to share code between iOS and Android, most companies choose C++. It is a well known language with very good tooling, but it also has a lot of pitfalls. For one, it is a very complex language. It also makes it really easy to accidentally introduce memory leaks or segmentation faults; especially if you're used to automatic memory management via a GC (Kotlin) or Arc (Swift). It also looks quite different from modern language like Swift or Kotlin. Now that we iOS developers got (mostly) rid of Objective-C, and Android Developers got (mostly) rid of Java, it feels archaic having to go back to a language with an archaic Syntax like C++.
Rust looks and feels a lot like Kotlin or Swift, and it offers the same easy ways of sharing code as C++. In addition to that, Rust has a very safe memory management model, high performance, a way to do fearless concurrency, and a very rich package ecosystem. As a bonus, it compiles to WebAssembly, so the shared code could also be used in any HTML5 app. This talk showcases how one can share very performant cross platform code between iOS, Android and others by using Rust.

Fea6d57cccac4021b6c8acbfaa468965?s=128

Benedikt Terhechte

February 08, 2019
Tweet

Transcript

  1. 3.

    @terhechte • German Social Network • Based in Hamburg •

    15 Mio Users • Native Apps on all platforms
  2. 5.

    @terhechte 3 NATIVE PLATFORMS • Implement everything three times •

    Amount of bugs X 3 • Alignment Overhead iOS, ANDROID, WINDOWS
  3. 6.

    @terhechte FIND WAYS OF SHARING CODE • React Native •

    C# • Flutter • Model Layer in C++
  4. 7.

    @terhechte USE NATIVE LANGUAGE FOR UI* • Kotlin (or Java)

    for Android • Swift (or Objective-C) for iOS • C# (or C++) for Windows * For Complex Projects
  5. 8.
  6. 17.

    @terhechte C++ IS NOT BAD • Very complex language •

    Tricky finding good developers • Easy to introduce serious bugs
  7. 20.

    @terhechte WHAT DOES C++ OFFER? • Build for cross Platform

    Development • Very High Speed • Huge Standard Library
  8. 21.

    @terhechte • Still Young • Many Performance Optimizations yet to

    come • Many Packages are still JVM only • SDKs (i.e. Dropbox, Firebase) often don’t exist for Kotlin Native yet KOTLIN NATIVE
  9. 24.

    @terhechte A SUM OF STRINGS < 100 “54,28,42,77,34 ,90 ,

    8 , 120,76 , 52, 117 ,21 ,5” SPLIT, TRIM, TO INT, FILTER, SUM
  10. 25.

    @terhechte fun sumSplit(contents: String): Int { return contents.split(",").asSequence() .map({ str

    -> str.trim().toIntOrNull() }) .filterNotNull() .filter({ number -> number < 100 }) .fold(0, { a, b -> a + b}) } KOTLIN
  11. 26.

    @terhechte fn sum_and_split(contents: String) -> i32 { contents.split(",") .map(|s| {

    s.trim() }) .filter_map(|s| { s.parse().ok() }) .filter(|number: &i32| { *number < 100 }) .fold(0, |acc, x| acc + x) } RUST
  12. 27.

    @terhechte // Lifted from StackOverflow std::vector<std::string> split(const std::string &s, char

    delim) { std::stringstream ss(s); std::string item; std::vector<std::string> elems; while (std::getline(ss, item, delim)) { elems.push_back(std::move(item)); } return elems; } // Lifted from StackOverflow inline std::string trim(const std::string &s) { auto wsfront=std::find_if_not(s.begin(),s.end(),[](int c){return std::isspace(c);}); return std::string(wsfront,std::find_if_not(s.rbegin(),std::string::const_reverse_iterator(wsfront),[](int c){return std::isspace(c);}).base()); } inline std::optional<int> operate(const std::string &s) { auto trimmed = trim(s); try { auto number = std::stoi(trimmed); return number; } catch (int ) { return {}; } } int test(std::string str) { // I found it tricky to use a fully functional C++ version without external libraries such as // boost. // So, instead, enjoy this half-hearted attempt auto contents = split(str, ','); std::vector<std::optional<int>> converted; converted.resize(contents.size()); std::transform(contents.begin(), contents.end(), converted.begin(), operate); auto sum = 0; for (std::optional<int> n: converted) { if (n.has_value() ) { if (n.value() < 100) { sum += n.value(); } } } return sum; } C++
  13. 28.

    @terhechte 0s 3,5s 7s 10,5s 14s Rust C++ Kotlin 12,52s

    0,6s 0,31s Compile Strings Functional
  14. 29.

    @terhechte 0s 0,5s 1s 1,5s 2s Rust C++ Kotlin 1,82s

    0,1s 0,07s Run Strings Functional
  15. 32.

    @terhechte WHAT IS RUST • Started 2006 • Strongly Typed,

    Compiled • LLVM Based • Driven by Mozilla
  16. 34.

    @terhechte –Wikipedia “Rust is intended to be a language for

    highly concurrent and highly safe systems.”
  17. 35.

    @terhechte SOME PROJECTS USING IT • Firefox (Rendering, CSS) •

    Dropbox (Custom Server Filesystem) • Oracle (Container Runtime) • Yelp, CloudFlare, …
  18. 36.

    @terhechte CROSS PLATFORM • Is not at home on any

    platform • Firefox is using it. Firefox is per definition cross platform • Rust & Packages run equally well on Windows, Linux, Android, iOS, macOS, etc
  19. 37.

    @terhechte ARCHITECTURES x86, x86_64, i386, ARM, ARM64, WASM, MIPS, MIPS64,

    SPARC, RISC, 16bit MSP430, etc OPERATING SYSTEMS Windows, macOS, Linux, iOS, Android, FreeBSD, NetBSD, Haiku, Redox, Haiku, etc TARGETS WebAssembly, Ruby, Python, JVM, C, C++, more
  20. 39.

    @terhechte let vector = vec![1, 2, 3, 4]; for element

    in vector { println!("The number is: {}", element); } vector.iter() .for_each(|chr| println!("A Char: {}", chr) ); ARRAYS [Vectors]
  21. 43.

    @terhechte struct User { name: String, age: i32 } impl

    User { fn formatted(&self) -> String { return format!(“{} {}", self.name, self.age); } } STRUCT METHODS
  22. 44.

    @terhechte enum RGB { Red, Green, Blue } enum Expression

    { Const(i32), Sum(i32, i32), Text(String) } SEALED CLASSES / ENUMS [Enums]
  23. 46.
  24. 48.

    @terhechte trait Countable { fn counted() -> i32; } struct

    Number { number: i32 } impl Countable for Number { fn counted(&self) -> i32 { return self.number; } } INTERFACES
  25. 49.

    @terhechte • No Classes (But Reference Types and Interface Inheritance)

    • Limited Reflection (But Hygienic Macros) • No Function Overloading (But Operator Overloading)
  26. 51.

    @terhechte JUST CURSORY BENCHMARKS • Mac mini 2018, 3.2GHz •

    Idiomatic code • Will release the benchmark suite on Twitter
  27. 52.

    @terhechte VERSIONS • Kotlin Native 1.0.3 (6. Dec 2018) •

    Rust 1.32.0 (16. Jan 2019) • C++ Clang 6.0
  28. 55.

    @terhechte fun isPrime(number: Int): Boolean { return (num > 1)

    && !((2 until num) .any({ n -> num % n == 0 })) } fn is_prime(num: i32) -> bool { num > 1 && !(2..num) .any(|n| num % n == 0) } PRIME NUMBERS
  29. 56.
  30. 59.

    @terhechte fun resize_chunk(chunk: List<Int>, scale: Int): List<Int> { return chunk.chunked(scale)

    .map({ innerChunk -> innerChunk.sum() }) } fun resize_image(image: List<Int>, width: Int, scale: Int): List<Int> { return image.chunked(width) .map({ innerChunk -> resize_chunk(innerChunk, scale) }) .flatten() } CHUNKS flatten(chunked > map > resize_chunk > chunked > map > sum)
  31. 60.

    @terhechte fn resize_chunk<'a>(chunk: &'a [usize], scale: usize) -> impl Iterator<Item

    = usize> + 'a { chunk.chunks(scale) .map(|chunk| { chunk.iter().sum::<usize>() }) } fn resize_image(image: &[usize], width: usize, scale: usize) -> Vec<usize> { image.chunks(width) .map(|slice| resize_chunk(slice, scale)) .flatten() .collect() } CHUNKS flatten(chunks > map > resize_chunk > chunk > map > sum)
  32. 61.

    @terhechte uint64_t* resize_image(uint64_t* image, uint64_t size, uint64_t width, uint64_t scale,

    uint64_t* rsize) { uint64_t *result = malloc(sizeof(uint64_t) * (size / scale)); uint64_t pos = 0; for (uint64_t i=0; i<size; i+=width) { for (uint64_t i2=i; i2<(i + width); i2+=scale) { uint64_t sum = 0; for (uint64_t i3=i2; i3<(i2 + scale); i3+=1) { sum += image[i3]; } result[pos++] = sum; } } *rsize = pos; return result; } CHUNKS ©
  33. 62.

    @terhechte 0s 3,5s 7s 10,5s 14s Rust C Kotlin 13,92s

    0,12s 0,67s Compile Chunks Imperative
  34. 63.

    @terhechte 0s 40s 80s 120s 160s Rust C Kotlin 143,76s

    0,67s 0,76s Run Chunks Imperative
  35. 64.

    @terhechte fun resize_image(image: IntArray, width: Int, scale: Int): IntArray {

    val result = IntArray(image.size / scale) var pos = 0 for (i in 0 until image.size step width) { for (i2 in i until (i + width) step scale) { var sum = 0 for (i3 in i2 until (i2 + scale)) { sum += image[i3] } result[pos] = sum pos += 1 } } return result } CHUNKS
  36. 65.

    @terhechte 0s 1,5s 3s 4,5s 6s Rust C Kotlin 5,76s

    0,67s 0,76s Run Chunks Imperative
  37. 67.

    @terhechte #[derive(Deserialize)] struct Media { author: User, likes: Vec<User>, comments:

    Vec<Comment>, images: HashMap<String, String>, description: String } let media: Vec<Media> = serde_json::from_str(contents)?; RUST
  38. 68.

    @terhechte @Serializable data class Media( val author: User, val likes:

    Array<User>, val comments: Array<Comment>, val images: Map<String, String>, val description: String) val obj = Json.parse(Media.serializer().list, contents) KOTLIN
  39. 73.

    @terhechte WHAT? • Focus on Model Layer • Kotlin is

    much better than Rust at UI Code • Swift is much better than Rust at UI Code
  40. 74.

    @terhechte HOW? • For iOS: Compile Rust into C library.

    Use via Objective-C Bridge. • For Android: Compile Rust via Swig
  41. 78.

    @terhechte WHY? • Multicore devices are ever more common •

    Especially in the model, you want to use all the cores as best as possible • Concurrency bugs (Data Races) are really easy most languages
  42. 79.

    @terhechte let mut data = vec![1, 2, 3]; for i

    in 0..100 { thread::spawn(|| { data.push(i); }); } RUST: to force the closure to take ownership of `data` use the `move` keyword
  43. 80.

    @terhechte let mut data = vec![1, 2, 3]; for i

    in 0..100 { thread::spawn(move || { data.push(i); }); } RUST: capture of moved value `data`
  44. 81.

    @terhechte let mut data = Arc::new(vec![1, 2, 3]); for i

    in 0..100 { let data_ref = data.clone(); thread::spawn(move || { data_ref.push(i); }); } RUST: cannot borrow data_ref as mutable. Arc is immutable
  45. 82.

    @terhechte let mut data = Arc::new(Mutex::new(vec![1, 2])); for i in

    0..100 { let data_ref = data.clone(); thread::spawn(move || { let mut data = data_ref.lock.unwrap(); data_ref.push(i); }); } RUST: I Compile
  46. 84.

    @terhechte • Rust is really powerful at multicore programming •

    It also becomes much easier to write multicore code • Ecosystem is constantly improving
  47. 86.

    @terhechte OTHER THINGS • Hygienic Macros with IDE support •

    Go-Like Channels in the standard library • Futures (async await) coming soon • SIMD Support • Limited support for using C++ libraries
  48. 87.

    @terhechte OTHER THINGS • Really fast web frameworks • Fantastic

    package manager • Very welcoming community
  49. 88.

    @terhechte UPSIDES • Easier & Safer than C++ • Much

    more similar to Kotlin than to C++ • Really Fast • Generated binaries are kinda small (starting from ~500kb)
  50. 89.

    @terhechte UPSIDES • Consumes very little memory • Excels at

    concurrency • Great, cross-platform ecosystem • Also share code with Ruby, Python, Backend etc
  51. 90.

    @terhechte DOWNSIDES • No classes, no easy shared mutable state

    • Initial learning phase is hard (Memory Management) • Difficult to hire but much easier to learn than C++ • Tooling for Android / iOS is especially young & verbose
  52. 92.
  53. 93.

    @terhechte WHERE IS C++ NEEDED • Complex projects with lots

    of on-device logic • PDF Parsing (PSPDFKit) • Cross Platform DRM Music Player (Spotify) • Office Suite, Image Editing, etc
  54. 94.

    @terhechte IF YOU NEED SPEED, CONCURRENCY, LOW MEMORY, & CROSS

    PLATFORM ABSTRACTIONS, THEN CONSIDER RUST INSTEAD OF C++
  55. 95.

    @terhechte POLITICS THE IOS GUYS WANT SWIFT THE ANDROID GUYS

    WANT KOTLIN RUST IS THE SAFE MIDDLE ;)
  56. 96.