Skip to content

Commit

Permalink
Add codegen backend for Ruby.
Browse files Browse the repository at this point in the history
This commit adds support for `uniffi-bindgen -l ruby` to generate
a Ruby module from a UniFFI Rust component.
  • Loading branch information
saks authored and rfk committed May 26, 2021
1 parent ed962e8 commit 7a26b71
Show file tree
Hide file tree
Showing 31 changed files with 1,790 additions and 4 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

### What's New

- A new **Ruby** codegen backend has been added. You can now call `uniffi-bindgen -l ruby` to
generate a Ruby module that wraps a UniFFI Rust component. Thanks to @saks for contributing
this backend!
- When running `cargo test` locally, you will need a recent version of Ruby and
the `ffi` gem in order to successfully execute the Ruby backend tests.
- Threadsafe Object methods can now use `self: Arc<Self>` as the method receiver in the underlying
Rust code, in addition to the default `self: &Self`. To do so, annotate the method with
`[Self=ByArc]` in the `.udl` file and update the corresponding Rust method signature to match.
Expand Down
31 changes: 31 additions & 0 deletions examples/arithmetic/tests/bindings/test_arithmetic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require 'test/unit'
require 'arithmetic'

include Test::Unit::Assertions

assert_raise Arithmetic::ArithmeticError::IntegerOverflow do
Arithmetic.add 18_446_744_073_709_551_615, 1
end

assert_equal Arithmetic.add(2, 4), 6
assert_equal Arithmetic.add(4, 8), 12

assert_raise Arithmetic::ArithmeticError::IntegerOverflow do
Arithmetic.sub 0, 1
end

assert_equal Arithmetic.sub(4, 2), 2
assert_equal Arithmetic.sub(8, 4), 4
assert_equal Arithmetic.div(8, 4), 2

assert_raise Arithmetic::InternalError do
Arithmetic.div 8, 0
end

assert Arithmetic.equal(2, 2)
assert Arithmetic.equal(4, 4)

assert !Arithmetic.equal(2, 4)
assert !Arithmetic.equal(4, 8)
1 change: 1 addition & 0 deletions examples/arithmetic/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
uniffi_macros::build_foreign_language_testcases!(
"src/arithmetic.udl",
[
"tests/bindings/test_arithmetic.rb",
"tests/bindings/test_arithmetic.py",
"tests/bindings/test_arithmetic.kts",
"tests/bindings/test_arithmetic.swift",
Expand Down
3 changes: 3 additions & 0 deletions examples/arithmetic/uniffi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ cdylib_name = "arithmetical"
[bindings.python]
cdylib_name = "arithmetical"

[bindings.ruby]
cdylib_name = "arithmetical"

[bindings.swift]
cdylib_name = "arithmetical"
16 changes: 16 additions & 0 deletions examples/geometry/tests/bindings/test_geometry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

require 'test/unit'
require 'geometry'

include Test::Unit::Assertions
include Geometry

ln1 = Line.new(Point.new(0.0, 0.0), Point.new(1.0, 2.0))
ln2 = Line.new(Point.new(1.0, 1.0), Point.new(2.0, 2.0))

assert_equal Geometry.gradient(ln1), 2
assert_equal Geometry.gradient(ln2), 1

assert_equal Geometry.intersection(ln1, ln2), Point.new(0, 0)
assert Geometry.intersection(ln1, ln1).nil?
1 change: 1 addition & 0 deletions examples/geometry/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ uniffi_macros::build_foreign_language_testcases!(
"src/geometry.udl",
[
"tests/bindings/test_geometry.py",
"tests/bindings/test_geometry.rb",
"tests/bindings/test_geometry.kts",
"tests/bindings/test_geometry.swift",
]
Expand Down
142 changes: 142 additions & 0 deletions examples/rondpoint/tests/bindings/test_rondpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# frozen_string_literal: true

require 'test/unit'
require 'rondpoint'

include Test::Unit::Assertions
include Rondpoint

dico = Dictionnaire.new Enumeration::DEUX, true, 0, 123_456_789

assert_equal dico, Rondpoint.copie_dictionnaire(dico)

assert_equal Rondpoint.copie_enumeration(Enumeration::DEUX), Enumeration::DEUX

assert_equal Rondpoint.copie_enumerations([
Enumeration::UN,
Enumeration::DEUX
]), [Enumeration::UN, Enumeration::DEUX]

assert_equal Rondpoint.copie_carte({
'0' => EnumerationAvecDonnees::ZERO.new,
'1' => EnumerationAvecDonnees::UN.new(1),
'2' => EnumerationAvecDonnees::DEUX.new(2, 'deux')
}), {
'0' => EnumerationAvecDonnees::ZERO.new,
'1' => EnumerationAvecDonnees::UN.new(1),
'2' => EnumerationAvecDonnees::DEUX.new(2, 'deux')
}

assert Rondpoint.switcheroo(false)

assert_not_equal EnumerationAvecDonnees::ZERO.new, EnumerationAvecDonnees::UN.new(1)
assert_equal EnumerationAvecDonnees::UN.new(1), EnumerationAvecDonnees::UN.new(1)
assert_not_equal EnumerationAvecDonnees::UN.new(1), EnumerationAvecDonnees::UN.new(2)

# Test the roundtrip across the FFI.
# This shows that the values we send come back in exactly the same state as we sent them.
# i.e. it shows that lowering from ruby and lifting into rust is symmetrical with
# lowering from rust and lifting into ruby.
RT = Retourneur.new

def affirm_aller_retour(vals, fn_name)
vals.each do |v|
id_v = RT.public_send fn_name, v

assert_equal id_v, v, "Round-trip failure: #{v} => #{id_v}"
end
end

MIN_I8 = -1 * 2**7
MAX_I8 = 2**7 - 1
MIN_I16 = -1 * 2**15
MAX_I16 = 2**15 - 1
MIN_I32 = -1 * 2**31
MAX_I32 = 2**31 - 1
MIN_I64 = -1 * 2**31
MAX_I64 = 2**31 - 1

# Ruby floats are always doubles, so won't round-trip through f32 correctly.
# This truncates them appropriately.
F32_ONE_THIRD = [1.0 / 3].pack('f').unpack('f')[0]

# Booleans
affirm_aller_retour([true, false], :identique_boolean)

# Bytes.
affirm_aller_retour([MIN_I8, -1, 0, 1, MAX_I8], :identique_i8)
affirm_aller_retour([0x00, 0x12, 0xFF], :identique_u8)

# Shorts
affirm_aller_retour([MIN_I16, -1, 0, 1, MAX_I16], :identique_i16)
affirm_aller_retour([0x0000, 0x1234, 0xFFFF], :identique_u16)

# Ints
affirm_aller_retour([MIN_I32, -1, 0, 1, MAX_I32], :identique_i32)
affirm_aller_retour([0x00000000, 0x12345678, 0xFFFFFFFF], :identique_u32)

# Longs
affirm_aller_retour([MIN_I64, -1, 0, 1, MAX_I64], :identique_i64)
affirm_aller_retour([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], :identique_u64)

# Floats
affirm_aller_retour([0.0, 0.5, 0.25, 1.0, F32_ONE_THIRD], :identique_float)

# Doubles
affirm_aller_retour(
[0.0, 0.5, 0.25, 1.0, 1.0 / 3, Float::MAX, Float::MIN],
:identique_double
)

# Strings
affirm_aller_retour(
['', 'abc', 'été', 'ښي لاس ته لوستلو لوستل',
'😻emoji 👨‍👧‍👦multi-emoji, 🇨🇭a flag, a canal, panama'],
:identique_string
)

# Test one way across the FFI.
#
# We send one representation of a value to lib.rs, and it transforms it into another, a string.
# lib.rs sends the string back, and then we compare here in ruby.
#
# This shows that the values are transformed into strings the same way in both ruby and rust.
# i.e. if we assume that the string return works (we test this assumption elsewhere)
# we show that lowering from ruby and lifting into rust has values that both ruby and rust
# both stringify in the same way. i.e. the same values.
#
# If we roundtripping proves the symmetry of our lowering/lifting from here to rust, and lowering/lifting from rust to here,
# and this convinces us that lowering/lifting from here to rust is correct, then
# together, we've shown the correctness of the return leg.
ST = Stringifier.new

def affirm_enchaine(vals, fn_name)
vals.each do |v|
str_v = ST.public_send fn_name, v

assert_equal v.to_s, str_v, "String compare error #{v} => #{str_v}"
end
end

# Test the efficacy of the string transport from rust. If this fails, but everything else
# works, then things are very weird.
assert_equal ST.well_known_string('ruby'), 'uniffi 💚 ruby!'

# Booleans
affirm_enchaine([true, false], :to_string_boolean)

# Bytes.
affirm_enchaine([MIN_I8, -1, 0, 1, MAX_I8], :to_string_i8)
affirm_enchaine([0x00, 0x12, 0xFF], :to_string_u8)

# Shorts
affirm_enchaine([MIN_I16, -1, 0, 1, MAX_I16], :to_string_i16)
affirm_enchaine([0x0000, 0x1234, 0xFFFF], :to_string_u16)

# Ints
affirm_enchaine([MIN_I32, -1, 0, 1, MAX_I32], :to_string_i32)
affirm_enchaine([0x00000000, 0x12345678, 0xFFFFFFFF], :to_string_u32)

# Longs
affirm_enchaine([MIN_I64, -1, 0, 1, MAX_I64], :to_string_i64)
affirm_enchaine([0x0000000000000000, 0x1234567890ABCDEF, 0xFFFFFFFFFFFFFFFF], :to_string_u64)
1 change: 1 addition & 0 deletions examples/rondpoint/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ uniffi_macros::build_foreign_language_testcases!(
"tests/bindings/test_rondpoint.kts",
"tests/bindings/test_rondpoint.swift",
"tests/bindings/test_rondpoint.py",
"tests/bindings/test_rondpoint.rb",
]
);
22 changes: 22 additions & 0 deletions examples/sprites/tests/bindings/test_sprites.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

require 'test/unit'
require 'sprites'

include Test::Unit::Assertions
include Sprites

sempty = Sprite.new(nil)
assert_equal sempty.get_position, Point.new(0, 0)

s = Sprite.new(Point.new(0, 1))
assert_equal s.get_position, Point.new(0, 1)

s.move_to(Point.new(1, 2))
assert_equal s.get_position, Point.new(1, 2)

s.move_by(Vector.new(-4, 2))
assert_equal s.get_position, Point.new(-3, 4)

srel = Sprite.new_relative_to(Point.new(0, 1), Vector.new(1, 1.5))
assert_equal srel.get_position, Point.new(1, 2.5)
1 change: 1 addition & 0 deletions examples/sprites/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ uniffi_macros::build_foreign_language_testcases!(
"src/sprites.udl",
[
"tests/bindings/test_sprites.py",
"tests/bindings/test_sprites.rb",
"tests/bindings/test_sprites.kts",
"tests/bindings/test_sprites.swift",
]
Expand Down
30 changes: 30 additions & 0 deletions examples/todolist/tests/bindings/test_todolist.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require 'test/unit'
require 'todolist'

include Test::Unit::Assertions
include Todolist

todo = TodoList.new
entry = TodoEntry.new 'Write bindings for strings in records'

todo.add_item('Write ruby bindings')

assert_equal todo.get_last, 'Write ruby bindings'

todo.add_item('Write tests for bindings')

assert_equal todo.get_last, 'Write tests for bindings'

todo.add_entry(entry)

assert_equal todo.get_last, 'Write bindings for strings in records'
assert_equal todo.get_last_entry.text, 'Write bindings for strings in records'

todo.add_item("Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣")
assert_equal todo.get_last, "Test Ünicode hàndling without an entry can't believe I didn't test this at first 🤣"

entry2 = TodoEntry.new("Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣")
todo.add_entry(entry2)
assert_equal todo.get_last_entry.text, "Test Ünicode hàndling in an entry can't believe I didn't test this at first 🤣"
1 change: 1 addition & 0 deletions examples/todolist/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ uniffi_macros::build_foreign_language_testcases!(
[
"tests/bindings/test_todolist.kts",
"tests/bindings/test_todolist.swift",
"tests/bindings/test_todolist.rb",
"tests/bindings/test_todolist.py"
]
);
Loading

0 comments on commit 7a26b71

Please sign in to comment.