# What is the "cross-platform" API

Flutter has a file `core.dart` which exposes an API which is usable on Wasm and native platforms. Each platform’s implementation depends on core libraries which aren’t available on the other (`dart:ffi` is native only, while `dart:js_interop` is web only).

As such, we need to expose something which is free from any types from any platform-exclusive core library.

This document is an attempt to collect the “rules” for what the cross-platform API should contain, and what it should not contain.

## General

In general, we try to mimic the API generated by `ffigen` found in `generated_bindings.dart` as closely as possible. This isn’t always possible/ideal, so exceptions are listed below.

## Pointers

`Pointer<T>` is from `dart:ffi`, so we need our own pointer type. It is called `CPPointer<T>` where `T` is a subtype of `CPPointerTarget`. In Rust terms, `CPPointerTarget` is effectively a “marker trait”, and all the subtypes of it (`CPDitto`, `CPIdentityConfig`, etc.) are ZSTs that are only used as type parameters to `CPPointer<T>`

`CPPointer` is an abstract class with separate WASM and native implementations, both of which are totally opaque. We never do pointer arithmetic or any other operation on the data of a pointer. We only ever receive them from core, and pass them to core.

### Avoid "out pointers"

Some APIs use "out pointers", which are pointers that are passed as arguments whose only purpose is for the function to write data to. For example:
```rust
fn do_something(bytes_written: &mut u64) {
    // do stuff...

    *bytes_written = 123;
}
```
In Dart, we would consume this API like this:
```dart
final bytesWritten = malloc<Uint64>();
bindings.do_something(bytes_written);

expect(bytesWritten.ref, equals(123));
```
Unfortunately, `malloc` is from `package:ffi` (which depends on `dart:ffi`), so cannot be used by consumers of the cross-platform API.

We could expose `malloc` as part of the cross-platform API:
```dart
CPPointer<T> malloc<T extends CPPointerTarget>([int count = 1]) => ...;
```
But this is risky, because we don't preserve type safety between cross-platform pointers and regular pointers.
Internally, a regular pointer just stores a `Pointer<Void>`, because there's no way to model the bidirectional mapping between cross-platform and regular types.
Essentially we'd need a trait with an associated type, which Dart doesn't support.

So instead of exposing this API as-is, we treat out pointers as return values.
Dart supports records (tuples in Rust terms), which can simulate multiple returns.
So the following Rust function:
```rust
// a, b are regular parameters
// out_1 and out_2 are out pointers
// it returns a bool
fn do_thing(a: i32, b: i32, out_1: &mut i32, out_2: &mut i32) -> bool {}
```
would give a Dart cross-platform function with the signature `(bool, int, int) Function(int, int)`, which could be implemented in native like so:
```dart
(bool, int, int) doThing(int a, int b) {
    final out1 = malloc<Int32>();
    final out2 = malloc<Int32>();

    final result = bindings.do_thing(a, b, out1, out2);
    return (result, out1.ref, out2.ref);
}
```

### A note on finalization

Finalization (a.k.a. destructors) behaves a little different in Dart compared to Rust. Instead of being a property of a type, it is a property of a value. A `Finalizer` can be attached to a specific object (not class). The type itself must also implement `Finalizable`, but simply implementing `Finalizable` does very little without attaching a `Finalizer`.

Correctly attaching finalizers **is the responsibility of each platform’s implementation**. This is because native code actually uses `NativeFinalizer` instead of `Finalizer`, which is from `dart:ffi`.

In other words, code outside of `core`  can assume that finalizers are attached to instances of `CPPointer` as required, such that dereferencing a `CPPointer` is always safe. If we need some mechanism to have early finalization, I’m not opposed to adding a flag to `CPPointer` and throwing/returning `null` if it’s invalid (similar to how `WeakReference` works)

## `String` / `Uint8List`

Strings and bytes get special treatment. While they could feasibly be implemented as `CPPointer<CPChar>` or `CPPointer<CPUint8>`, two factors mean that it makes more sense to have the plain Dart types in the interface:

- Wasm has special handling, which means that a cross-platform implementation of a `CPPointer<CPChar>` is harder than it seems,
- They are handled slightly differently by different FFI functions, so we still have to do mapping in most cases (is a `String` a `char*` or a `slice_uint8_ref`?).

## Functions

Similarly to `String`s, regular Dart functions are OK as part of the cross-platform API. It’s up to each implementation to correctly convert them to something their core implementation can handle (`NativeCallable` for native, `JSFunction` for WASM).

Function types must be fully declared, i.e.:

```dart
// bad
void takesCallback(Function callback) { /* stuff */ }

// good
void takesCallback(void Function(int) callback) { /* stuff */ }

// better
void takesCallback(void Function(int seconds) callback) { /* stuff */ }
```

We’re not super consistent about this in the SDK, since the linter rules aren’t great, but it’s something we should focus on in new code.

## Async

The cross-platform abstraction should only expose async-ness where it is required by one of the platforms. In other words, if the C header has a continuation-style API, we should implement that in the shared code that consumes the cross-platform API:

```dart
// bad
Future<void> resolvesAsync() { /* ... */ }

// good
void resolvesAsync(void Function() onComplete) { /* .. */ }
```

The reasoning behind this is that, if both WASM and native provide a completion-based API, if we implement the Future-returning function twice, we’re duplicating work.

In the case where Wasm doesn’t provide a synchronous function, then we can implement the other side in Dart to provide a shared `Future`-returning API, but that’s only if one side **only offers a `Future`-returning API**.
