Skip to content

cpp_extern for non-constant number of arguments #900

@mbouaziz

Description

@mbouaziz

Because the different fields of a value class are passed as several parameters instead of one, a developer writing a cpp_extern function may not get the expected result.
For example, if you have this function:

@cpp_extern
native fun f<T>(x: T): void;

Calls to f are made with:

  • one parameter if T is an immediate value (e.g. Int) or a pointer (non-value class object)
  • zero parameter if T is void or any other zero-(flattened)-field value class object
  • one parameter if T is a one-(flattened)-field value class object
  • two parameters if T is a two-(flattened)-field value class object, e.g. (Int, Int)
  • ...

which will currently result in a silent wrong call in native or an unreachable exception in wasm.

There are several things that could be done here.

We could assume someone writing a cpp_extern function for an explicitly written value class knows what they're doing and do nothing in this case, e.g. native fun g(x: (Int, Int)). But there will definitely be issues if the call may give several number of parameters, like f above if called with void and then Int.
We could then issue an error if and only if the calls are done with different numbers of parameters.

Though, we would not give errors if all calls are done with the same number of parameters.
But if someone changes the call to another actual number of parameters, they may forget to update the C code.
Another possibility then is to error when the function may have different number of parameters, which would basically exclude generics not constrained by a base class.

But then, how do I write native_eq?
The first solution is to always box the parameters:

@cpp_extern
native fun unsafe_native_eq<T>(x: T, y: T): Bool;

fun native_eq<T>(x: T, y: T): Bool {
  unsafe_native_eq(Box(x), Box(y))
}

class Box<T>(T)

But this sadly allocates all the time! We want to box only if necessary. This would require function (and method!) overloading, something like:

@cpp_extern
native fun single_native_eq<T: single>(x: T, y: T): Bool;
// single means T will expand to exactly one parameter: immediate, pointer, single-(flattened)-field value class

fun native_eq<T>(x: T, y: T): Bool
  [T: single]{ single_native_eq(x, y) }
  [T: empty]{ true } // e.g. void
  [fallback]{ single_native_eq(Box(x), Box(y)) }

To avoid several cases to match, the semantics is sequential testing: if the first constraint is fulfilled, then the first implementation is used, else ...

The above implementation is actually wrong because we need to separate the immediate and the pointer cases:

@cpp_extern
native fun imm_native_eq<T: immediate>(x: T, y: T): Bool;

@cpp_extern
native fun ptr_native_eq<T: pointer>(x: T, y: T): Bool;
...

immediate and pointer are sub-types (or rather sub-constraints) of single

Going further, we could generalize empty and single: object2, ... but then how do you differentiate pairs where both elements are immediate from those with an immediate and a pointer? And how do you refer to those elements in the function body?

@cpp_extern
native fun single_phys_eq<T: single>(x: T, y: T): Bool;

fun phys_eq<T>(x: T, y: T): Bool [T: object2]{ single_phys_eq(x.i0, y.i0) && single_phys_eq(x.i1, y.i1) } ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions