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. @terhechte

  2. @terhechte ABOUT ME Benedikt Terhechte @terhechte www.appventure.me iOS / macOS

    / Linux Developer
  3. @terhechte • German Social Network • Based in Hamburg •

    15 Mio Users • Native Apps on all platforms
  4. @terhechte

  5. @terhechte 3 NATIVE PLATFORMS • Implement everything three times •

    Amount of bugs X 3 • Alignment Overhead iOS, ANDROID, WINDOWS
  6. @terhechte FIND WAYS OF SHARING CODE • React Native •

    C# • Flutter • Model Layer in C++
  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
  8. @terhechte USE C++ FOR SHARED MODEL • Facebook • Spotify

    • SoundCloud • PSPDFKit • …
  9. @terhechte WHY NOT C++?

  10. @terhechte IMAGES.GOOGLE.COM C++ MEME

  11. @terhechte

  12. @terhechte

  13. @terhechte

  14. @terhechte

  15. @terhechte

  16. @terhechte

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

    Tricky finding good developers • Easy to introduce serious bugs
  18. @terhechte WHY NOT C++?

  19. @terhechte Why not Kotlin Native?

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

    Development • Very High Speed • Huge Standard Library
  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
  22. @terhechte WHAT ABOUT RUST?

  23. @terhechte QUICK PERFORMANCE COMPARISON

  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
  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
  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
  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++
  28. @terhechte 0s 3,5s 7s 10,5s 14s Rust C++ Kotlin 12,52s

    0,6s 0,31s Compile Strings Functional
  29. @terhechte 0s 0,5s 1s 1,5s 2s Rust C++ Kotlin 1,82s

    0,1s 0,07s Run Strings Functional
  30. @terhechte RUST IS CRAZY FAST (C++ IS FASTER)

  31. @terhechte

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

    Compiled • LLVM Based • Driven by Mozilla
  33. @terhechte Mascot: Ferris

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

    highly concurrent and highly safe systems.”
  35. @terhechte SOME PROJECTS USING IT • Firefox (Rendering, CSS) •

    Dropbox (Custom Server Filesystem) • Oracle (Container Runtime) • Yelp, CloudFlare, …
  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
  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
  38. @terhechte

  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]
  40. @terhechte let mut vector: Vec<i32> = Vec::new(); vector.push(5); MUTABILITY

  41. @terhechte let mut string = "hey! 檀れい"; string.make_ascii_uppercase(); assert!(string ==

    "HEY! 檀れい"); MUTABILITY
  42. @terhechte fn add_two(first: i32, second: i32) -> i32 { return

    first + second; } FUNCTIONS
  43. @terhechte struct User { name: String, age: i32 } impl

    User { fn formatted(&self) -> String { return format!(“{} {}", self.name, self.age); } } STRUCT METHODS
  44. @terhechte enum RGB { Red, Green, Blue } enum Expression

    { Const(i32), Sum(i32, i32), Text(String) } SEALED CLASSES / ENUMS [Enums]
  45. @terhechte struct Container<T> { value: T } let contained =

    Container { value: 15 } GENERICS
  46. @terhechte fn add_anything<N: Add<Output=N>>(a: N, b: N) -> N {

    return a + b; } GENERIC CONSTRAINTS
  47. @terhechte trait Countable { fn counted() -> i32; } INTERFACES

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

    Number { number: i32 } impl Countable for Number { fn counted(&self) -> i32 { return self.number; } } INTERFACES
  49. @terhechte • No Classes (But Reference Types and Interface Inheritance)

    • Limited Reflection (But Hygienic Macros) • No Function Overloading (But Operator Overloading)
  50. @terhechte ! BENCHMARKS ARE THE DEVIL! Code Examples & Benchmarks

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

    Idiomatic code • Will release the benchmark suite on Twitter
  52. @terhechte VERSIONS • Kotlin Native 1.0.3 (6. Dec 2018) •

    Rust 1.32.0 (16. Jan 2019) • C++ Clang 6.0
  53. @terhechte KOTLIN: FUN RUST: FN

  54. @terhechte FINDING PRIME NUMBERS And awful solution using functional programming

  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
  56. @terhechte 0s 3s 6s 9s 12s Rust C++ Kotlin 11,26s

    0,35s 0,53s Compile Primes
  57. @terhechte 0s 25s 50s 75s 100s Rust C++ Kotlin 90,39s

    0,85s 0,85s Run Primes
  58. @terhechte CHUNKS OF NUMBERS

  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)
  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)
  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 ©
  62. @terhechte 0s 3,5s 7s 10,5s 14s Rust C Kotlin 13,92s

    0,12s 0,67s Compile Chunks Imperative
  63. @terhechte 0s 40s 80s 120s 160s Rust C Kotlin 143,76s

    0,67s 0,76s Run Chunks Imperative
  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
  65. @terhechte 0s 1,5s 3s 4,5s 6s Rust C Kotlin 5,76s

    0,67s 0,76s Run Chunks Imperative
  66. @terhechte JSON Parse JSON into an array of structs

  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
  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
  69. @terhechte 0s 4s 8s 12s 16s Rust Kotlin 15,82s 0,17s

    27MB JSON into Structs
  70. @terhechte 0MB 100MB 200MB 300MB 400MB Rust Kotlin 331MB 6MB

    Average Memory
  71. @terhechte • Very similar Code • Much faster • Less

    Memory
  72. @terhechte DEMO

  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
  74. @terhechte HOW? • For iOS: Compile Rust into C library.

    Use via Objective-C Bridge. • For Android: Compile Rust via Swig
  75. @terhechte DEMO

  76. @terhechte

  77. @terhechte CONCURRENCY FEARLESS* CONCURRENCY * Honestly, that’s what the Rust

    people call it
  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
  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
  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`
  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
  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
  83. @terhechte use rayon::prelude::*; fn sum_of_squares(input: &Vec<i32>) -> i32 { input.par_iter()

    // <-- just change that! .map(|&i| i * i) .sum() }
  84. @terhechte • Rust is really powerful at multicore programming •

    It also becomes much easier to write multicore code • Ecosystem is constantly improving
  85. @terhechte

  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
  87. @terhechte OTHER THINGS • Really fast web frameworks • Fantastic

    package manager • Very welcoming community
  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)
  89. @terhechte UPSIDES • Consumes very little memory • Excels at

    concurrency • Great, cross-platform ecosystem • Also share code with Ruby, Python, Backend etc
  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
  91. @terhechte WHAT NOW?

  92. @terhechte IF YOU’RE JUST PARSING JSON FROM THE SERVER, FOR

    THE LOVE OF GOD, DON’T USE RUST.
  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
  94. @terhechte IF YOU NEED SPEED, CONCURRENCY, LOW MEMORY, & CROSS

    PLATFORM ABSTRACTIONS, THEN CONSIDER RUST INSTEAD OF C++
  95. @terhechte POLITICS THE IOS GUYS WANT SWIFT THE ANDROID GUYS

    WANT KOTLIN RUST IS THE SAFE MIDDLE ;)
  96. @terhechte Benedikt Terhechte - twitter.com/terhechte Podcast: contravariance.rocks Questions? I have

    100 more slides XING is HIRING IN GERMANY, SPAIN, AND PORTUGAL