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: Opaque Apex class
Section titled “extern type: Opaque Apex class”extern type Messaging.SingleEmailMessageextern type Schema.DescribeSObjectResultpub extern type Limitsextern 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: Apex static method
Section titled “extern fn: Apex static method”extern fn Limits.getCpuTime(): Intextern fn Messaging.reserveSingleEmailCapacity(count: Int): Voidextern 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();}= "apexName" override
Section titled “= "apexName" override”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 new: Apex constructor
Section titled “extern new: Apex constructor”extern type Messaging.SingleEmailMessage
extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessageVertex code calls it using TypePath.new():
let msg = Messaging.SingleEmailMessage.new()Generates: Messaging.SingleEmailMessage msg = new Messaging.SingleEmailMessage();
Named constructor overloads
Section titled “Named constructor overloads”extern new Messaging.SingleEmailMessage(): Messaging.SingleEmailMessageextern new Messaging.SingleEmailMessage.withSubject( subject: String,): Messaging.SingleEmailMessageCall sites:
let plain = Messaging.SingleEmailMessage.new()let subj_msg = Messaging.SingleEmailMessage.withSubject("Hello")Generates:
new Messaging.SingleEmailMessage()new Messaging.SingleEmailMessage('Hello')extern method: Apex instance method
Section titled “extern method: Apex instance method”extern method Messaging.SingleEmailMessage.setSubject( self: Messaging.SingleEmailMessage, subject: String,): VoidThe 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: Read-only Apex field
Section titled “extern field: Read-only Apex field”extern field Messaging.InboundEmail.subject: StringVertex code can read the field via dot access; writes are rejected by the
checker (error E120).
extern var: Read-write Apex field
Section titled “extern var: Read-write Apex field”extern var Messaging.SingleEmailMessage.optOutPolicy: StringReads 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: Apex enum type
Section titled “extern enum: Apex enum type”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 = AddressMatch 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.
FFI safety rules
Section titled “FFI safety rules”Option<T> return: null-check wrapper
Section titled “Option<T> return: null-check wrapper”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:
| Field | Type | Description |
|---|---|---|
message | String | Exception message |
typeName | String | Apex exception class name |
stackTrace | String | Stack trace string |
cause | Option<ApexException> | Cause if present |
Calling Apex APIs that use Object
Section titled “Calling Apex APIs that use Object”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.