Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Kotlin] Generate open classes and methods #1815

Merged
merged 3 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions fixtures/coverall/tests/bindings/test_coverall.kts
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,48 @@ assert(d.integer == 42UL)
Coveralls("test_bytes").use { coveralls ->
assert(coveralls.reverse("123".toByteArray(Charsets.UTF_8)).toString(Charsets.UTF_8) == "321")
}

// Test fakes using open classes

class FakePatch(private val color: Color): Patch(NoPointer) {
override fun `getColor`(): Color = color
}

class FakeCoveralls(private val name: String) : Coveralls(NoPointer) {
private val repairs = mutableListOf<Repair>()

override fun `addPatch`(patch: Patch) {
repairs += Repair(Instant.now(), patch)
}

override fun `getRepairs`(): List<Repair> {
return repairs
}
}

FakeCoveralls("using_fakes").use { coveralls ->
val patch = FakePatch(Color.RED)
coveralls.addPatch(patch)
assert(!coveralls.getRepairs().isEmpty())
}

FakeCoveralls("using_fakes_and_calling_methods_without_override_crashes").use { coveralls ->
var exception: Throwable? = null
try {
coveralls.cloneMe()
} catch (e: Throwable) {
exception = e
}
assert(exception != null)
}

Coveralls("using_fakes_with_real_objects_crashes").use { coveralls ->
val patch = FakePatch(Color.RED)
var exception: Throwable? = null
try {
coveralls.addPatch(patch)
} catch (e: Throwable) {
exception = e
}
assert(exception != null)
}
28 changes: 24 additions & 4 deletions uniffi_bindgen/src/bindings/kotlin/templates/ObjectRuntime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,26 @@ inline fun <T : Disposable?, R> T.use(block: (T) -> R) =
//
// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219
//
abstract class FFIObject(
protected val pointer: Pointer
): Disposable, AutoCloseable {
abstract class FFIObject: Disposable, AutoCloseable {

constructor(pointer: Pointer) {
this.pointer = pointer
}

/**
* This constructor can be used to instantiate a fake object.
*
* **WARNING: Any object instantiated with this constructor cannot be passed to an actual Rust-backed object.**
* Since there isn't a backing [Pointer] the FFI lower functions will crash.
* @param noPointer Placeholder value so we can have a constructor separate from the default empty one that may be
* implemented for classes extending [FFIObject].
*/
@Suppress("UNUSED_PARAMETER")
constructor(noPointer: NoPointer) {
this.pointer = null
}

protected val pointer: Pointer?

private val wasDestroyed = AtomicBoolean(false)
private val callCounter = AtomicLong(1)
Expand Down Expand Up @@ -152,7 +169,7 @@ abstract class FFIObject(
} while (! this.callCounter.compareAndSet(c, c + 1L))
// Now we can safely do the method call without the pointer being freed concurrently.
try {
return block(this.pointer)
return block(this.pointer!!)
} finally {
// This decrement always matches the increment we performed above.
if (this.callCounter.decrementAndGet() == 0L) {
Expand All @@ -161,3 +178,6 @@ abstract class FFIObject(
}
}
}

/** Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. */
object NoPointer
34 changes: 26 additions & 8 deletions uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@

{% include "Interface.kt" %}
bendk marked this conversation as resolved.
Show resolved Hide resolved

class {{ impl_class_name }}(
pointer: Pointer
) : FFIObject(pointer), {{ interface_name }}{
open class {{ impl_class_name }} : FFIObject, {{ interface_name }} {

constructor(pointer: Pointer): super(pointer)

/**
* This constructor can be used to instantiate a fake object.
*
* **WARNING: Any object instantiated with this constructor cannot be passed to an actual Rust-backed object.**
* Since there isn't a backing [Pointer] the FFI lower functions will crash.
* @param noPointer Placeholder value so we can have a constructor separate from the default empty one that may be
* implemented for classes extending [FFIObject].
*/
constructor(noPointer: NoPointer): super(noPointer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great docstring and I like the use of the singleton object to differentiate this from the regular constructor.


{%- match obj.primary_constructor() %}
{%- when Some with (cons) %}
Expand All @@ -25,8 +35,10 @@ class {{ impl_class_name }}(
* Clients **must** call this method once done with the object, or cause a memory leak.
*/
override protected fun freeRustArcPtr() {
rustCall() { status ->
_UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, status)
this.pointer?.let { ptr ->
rustCall() { status ->
_UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(ptr, status)
}
}
}

Expand All @@ -38,7 +50,9 @@ class {{ impl_class_name }}(
{%- endmatch -%}
{%- if meth.is_async() %}
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
override suspend fun {{ meth.name()|fn_name }}({%- call kt::arg_list_decl(meth) -%}){% match meth.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} {
override suspend fun {{ meth.name()|fn_name }}(
{%- call kt::arg_list_decl(meth) -%}
){% match meth.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name }}{% when None %}{%- endmatch %} {
return uniffiRustCallAsync(
callWithPointer { thisPtr ->
_UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}(
Expand Down Expand Up @@ -68,15 +82,19 @@ class {{ impl_class_name }}(
{%- else -%}
{%- match meth.return_type() -%}
{%- when Some with (return_type) -%}
override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}): {{ return_type|type_name }} =
override fun {{ meth.name()|fn_name }}(
{%- call kt::arg_list_protocol(meth) -%}
): {{ return_type|type_name }} =
callWithPointer {
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
}.let {
{{ return_type|lift_fn }}(it)
}

{%- when None -%}
override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}) =
override fun {{ meth.name()|fn_name }}(
{%- call kt::arg_list_protocol(meth) -%}
) =
callWithPointer {
{%- call kt::to_ffi_call_with_prefix("it", meth) %}
}
Expand Down