Skip to content

Apex FFI, Extern Declarations

Vertex can reference existing Apex types through extern declarations. Extern declarations are compile-time metadata only. They tell the type checker that a foreign type exists, and the codegen emits references to it verbatim. No Apex class is generated for an extern declaration.

extern type Messaging.SingleEmailMessage
extern type Schema.DescribeSObjectResult
pub extern type Limits

extern type declares an Apex class as an opaque Vertex type. The type can be used in parameter and return positions and as generic type arguments, but it has no constructors until extern new is declared.

extern type Messaging.SingleEmailMessage
fn wrapAll(
msg: Messaging.SingleEmailMessage,
): List<Messaging.SingleEmailMessage> {
[msg]
}

Generates:

private static List<Messaging.SingleEmailMessage> wrapAll(Messaging.SingleEmailMessage msg) {
return new List<Messaging.SingleEmailMessage>{msg};
}
extern fn Limits.getCpuTime(): Int
extern fn Messaging.reserveSingleEmailCapacity(count: Int): Void

extern fn declares an Apex static method as a callable Vertex function.

extern fn Limits.getCpuTime(): Int
fn logCpu(): Int {
Limits.getCpuTime()
}

Generates:

private static Integer logCpu() {
return Limits.getCpuTime();
}

When two Apex overloads share the same name but differ by arity, each can be declared as a distinct extern fn with a = "..." override:

extern fn Messaging.sendEmail(
mails: List<Messaging.SingleEmailMessage>,
): Void
extern fn Messaging.sendEmailChecked(
mails: List<Messaging.SingleEmailMessage>,
allOrNothing: Bool,
): Void = "Messaging.sendEmail"
extern type Messaging.SingleEmailMessage
extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage

Vertex code calls it using TypePath.new():

let msg = Messaging.SingleEmailMessage.new()

Generates: Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();

extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessage
extern new Messaging.SingleEmailMessage.withSubject(
subject: String,
): Messaging.SingleEmailMessage

Call sites:

let plain = Messaging.SingleEmailMessage.new()
let subj_msg = Messaging.SingleEmailMessage.withSubject("Hello")

Generates:

new Messaging.SingleEmailMessage()
new Messaging.SingleEmailMessage('Hello')
extern method Messaging.SingleEmailMessage.setSubject(
self: Messaging.SingleEmailMessage,
subject: String,
): Void

The first parameter must be named self and carry the receiver type. It is excluded from the call-site signature.

let msg = Messaging.SingleEmailMessage.new()
msg.setSubject("Hello")

When the Apex method name differs from the Vertex name, use a = "..." override:

extern method Messaging.InboundEmail.getFrom(
self: Messaging.InboundEmail,
): String = "getFromAddress"
extern field Messaging.InboundEmail.subject: String

Vertex code can read the field via dot access; writes are rejected by the checker (error E120).

extern var Messaging.SingleEmailMessage.optOutPolicy: String

Reads are always allowed. Writes require the receiver to be declared mutable let; writing through an immutable binding is error E119.

fn configurePolicy(msg: Messaging.SingleEmailMessage): Void {
mutable let m = msg
m.optOutPolicy = "SEND_OPT_OUT" // ok. Local binding is mutable
}

Field assignment syntax: receiver.field = value emits receiver.field = value; verbatim in Apex.

extern enum Schema.DisplayType { Address, Currency, Phone, Picklist, Reference }

Maps an Apex enum type into Vertex as a sum type with unit variants.

let dt: Schema.DisplayType = Address

Match arms use == comparison in Apex:

fn label(dt: Schema.DisplayType): String {
match dt {
Address => "mailing address",
Phone => "phone number",
Reference => "lookup to another object",
_ => "other",
}
}

Generates:

if (dt == Schema.DisplayType.Address) {
return 'mailing address';
} else if (dt == Schema.DisplayType.Phone) {
return 'phone number';
} else if (dt == Schema.DisplayType.Reference) {
return 'lookup to another object';
} else {
return 'other';
}

Extern enum variants can be constructed and matched in JIT mode without a Salesforce connection.

When an extern declaration returns Option<T>, the codegen wraps every call site with a null check:

extern fn Limits.getHeapSize(): Option<Int>
let heap = Limits.getHeapSize() // heap: Option<Int>

Generated Apex:

Integer vtxFfiRaw0 = Limits.getHeapSize();
Option vtxFfiResult0 = vtxFfiRaw0 != null ? (Option) new Some(vtxFfiRaw0) : (Option) new None();
final Option heap = vtxFfiResult0;

The programmer is responsible for knowing whether the Apex API can return null. If declared non-Option and the Apex side returns null, a NullPointerException propagates in Apex.

Result<T, ApexException> return: try/catch wrapper

Section titled “Result<T, ApexException> return: try/catch wrapper”

When an extern declaration returns Result<T, ApexException>, the codegen wraps every call site with a try/catch.

extern fn Database.countQuery(soql: String): Result<Int, ApexException>
let result = Database.countQuery("SELECT COUNT() FROM Account")
match result {
Ok(n) => debug "count: " + n.toString(),
Error(e) => debug "error: " + e.message,
}

The ApexException type carries:

FieldTypeDescription
messageStringException message
typeNameStringApex exception class name
stackTraceStringStack trace string
causeOption<ApexException>Cause if present

Some Apex APIs, such as Database.queryWithBindings, JSON.deserializeUntyped, and parts of Schema.describe*, take or return Apex’s universal Object type. See Apex.Object for the interop escape hatch that makes those APIs reachable without breaking Vertex’s nominal type system.