Zig’de (v0.16.0) yerleşik arayüz¹ bulunmamaktadır fakat bu, işlem anı seçimli köpbiçim² yapılamayacağı anlamına gelmez. Bu konuda William Wong’un Zig Interface Revisited başlıklı yazısını faydalı buluyorum. Ancak burada bire bir Wong’un uygulamasından söz etmek yerine, uygulamadaki ödünlerin özgeleştiği bir yaklaşım anlatılacaktır. Bundan dolayı öncelikle William Wong’un yaklaşımının incelenmesi önerilir.
Sözleşme
Sanal metot tablosunun ilk parçası, imzaları taşıyan bir sözleşme yapısıdır. Yapının her alanı sabit bir işlev işaretçisidir³ ve arayüzün sunacağı bir davranışa karşılık gelir. Davranışın hangi örnek üzerinde çalışacağı bu noktada bilinmediğinden ilk parametre silik tipli bir işaretçi, *anyopaque, olarak yazılır; kalan parametreler davranışın kendi ihtiyacına göre sıralanır.
const Contract = struct {
behavior0: *const fn (implementation: *anyopaque, ...) void,
behavior1: *const fn (implementation: *anyopaque, ...) void,
...
};
Arayüz
Arayüz, sözleşme tipini derleme zamanında parametre olarak alan bir fonksiyonun döndürdüğü tiptir. Döndürülen tip yalnızca iki alan taşır: sözleşmeye bir işaretçi ve uygulayıcı örneğine silik tipli bir işaretçi.
Yapıcı fonksiyon, uygulayıcı örneği işaretçisini anytype olarak alır. Parametre anytype olduğu için fonksiyon her uygulayıcı tipi başına ayrı örneklenir. Sanal metot tablosu, bu yapının derleme zamanında sabit alanı olarak kurulur, dolayısıyla tip başına bir kez hesaplanır ve program boyunca yaşar.
Tablonun kurulumu, sözleşme alanları üzerinde dönerek yapılır. Her alan için uygulayıcı tipindeki aynı adlı bildirimin adresi alınır ve @ptrCast ile sözleşmedeki imzaya dönüştürülür. Bu dönüşüm denetimsiz olduğundan, dönüşümden önce iki imza elle aşağıdaki ölçütlere göre karşılaştırılır:
- jeneriklik⁴,
- çağrı uzlaşımı⁵,
- dönüş tipi,
- parametre sayısı,
- ilk parametrenin arkayüz tipine ait değiştirilebilir tekil bir işaretçi oluşu,
- kalan parametrelerin birebir eşleşmesi.
Eşleşmeyen her durum derleme hatasına düşer. Böylece *anyopaque parametresinin çağrı anında uygulayıcı örneği olarak yorumlanmaması güvence altına alınır. Denetim mantığı uygulayıcı başına çoğalmadığından tek noktadan yönetilir; hata mesajları da bütün arayüzlerde aynı biçimde üretilir.
Arayüz, sözleşmedeki her davranışı çağıran sarıcı⁷ metotların tanımlanmasıyla bitirilir. Zig, derleme zamanında bildirim⁶ üretemediğinden bu metotlar elle yazılır; buna karşılık *anyopaque kullanıcıdan tamamen gizlenir. Bu sarıcı katman yalnızca tabloyu iletmekle sınırlı değildir, sözleşmede karşılığı olmayan davranış tanımlamaya da olanak sağlar.
const std = @import("std");
/// The caller owns the implementation and must keep it alive for as long as this table exists.
pub fn Type(comptime Contract: type) type {
return struct {
methods: *const Contract,
implementation: *anyopaque,
pub fn init(ptr: anytype) @This() {
const ImplementationType = @TypeOf(ptr.*);
const Static = struct {
const methods: Contract = blk: {
const contract_name = @typeName(Contract);
const impelementation_name = @typeName(ImplementationType);
var temporary: Contract = undefined;
const contract_fields = std.meta.fields(Contract);
for (contract_fields) |contract_field| {
const expected = @typeInfo(@typeInfo(contract_field.type).pointer.child).@"fn";
const actual = @typeInfo(@TypeOf(@field(ImplementationType, contract_field.name))).@"fn";
if (expected.is_generic != actual.is_generic) {
@compileError(
std.fmt.comptimePrint(
\\┏·Genericity must match.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected.is_generic,
impelementation_name,
actual.is_generic,
},
),
);
}
if (std.meta.activeTag(expected.calling_convention) !=
std.meta.activeTag(actual.calling_convention))
{
@compileError(
std.fmt.comptimePrint(
\\┏·Calling conventions must match.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected.calling_convention,
impelementation_name,
actual.calling_convention,
},
),
);
}
if (expected.return_type.? != actual.return_type.?) {
@compileError(
std.fmt.comptimePrint(
\\┏·Return types must match.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected.return_type.?,
impelementation_name,
actual.return_type.?,
},
),
);
}
if (expected.params.len != actual.params.len) {
@compileError(
std.fmt.comptimePrint(
\\┏·Argument lengths.
\\┣━▶ {s}.args.len : {any}
\\┗━▶ {s}.args.len : {any}
,
.{
contract_name,
expected.params.len,
impelementation_name,
actual.params.len,
},
),
);
}
const expected_self = ImplementationType;
const actual_self = switch (@typeInfo(actual.params[0].type.?)) {
.pointer => |p| blk0: {
if (p.is_const) {
@compileError(
std.fmt.comptimePrint(
\\┏·Implementation self pointer cannot be constant.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected_self,
impelementation_name,
p,
},
),
);
}
if (p.size != .one) {
@compileError(
std.fmt.comptimePrint(
\\┏·Implementation self pointer size must be one.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected_self,
impelementation_name,
p,
},
),
);
}
break :blk0 p.child;
},
else => |e| {
@compileError(
std.fmt.comptimePrint(
\\┏·First parameter must be self pointer.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected_self,
impelementation_name,
e,
},
),
);
},
};
if (expected_self != actual_self) {
@compileError(
std.fmt.comptimePrint(
\\┏·Implementation type unmatch on self parameter.
\\┣━▶ {s} : {any}
\\┗━▶ {s} : {any}
,
.{
contract_name,
expected_self,
impelementation_name,
actual_self,
},
),
);
}
for (expected.params[1..], actual.params[1..], 0..) |e, a, i| {
if (e.is_generic != a.is_generic) {
@compileError(
std.fmt.comptimePrint(
\\┏·Argument genericity must match.
\\┣━▶ {s}.args[{d}] : {any}
\\┗━▶ {s}.args[{d}] : {any}
,
.{
contract_name,
i,
e.is_generic,
impelementation_name,
i,
a.is_generic,
},
),
);
}
if (e.type.? != a.type.?) {
@compileError(
std.fmt.comptimePrint(
\\┏·Params must match.
\\┣━▶ {s}.args[{d}] : {any}
\\┗━▶ {s}.args[{d}] : {any}
,
.{
contract_name,
i,
e.type.?,
impelementation_name,
i,
a.type.?,
},
),
);
}
}
// Takes the address of a declaration on the type, not the instance.
@field(temporary, contract_field.name) = @ptrCast(
&@field(ImplementationType, contract_field.name),
);
}
break :blk temporary;
};
};
return .{
.implementation = @ptrCast(@alignCast(ptr)),
.methods = &Static.methods,
};
}
};
}
Kullanım
Aşağıdaki iki tip birbirinden ve arayüzden bağımsız yazılmıştır. Ortak yanları yalnızca write ve clear bildirimlerinin imzalarıdır.
const std = @import("std");
pub fn main() void {
var terminal: Terminal = .{};
var buffer: Buffer = .{};
const writers = [_]Writer{
Writer.init(&terminal),
Writer.init(&buffer),
};
for (writers) |writer| {
writer.clear();
writer.write("merhaba");
writer.writeFmt("deger: {d}", .{42});
}
std.debug.print("buffer: {s}\n", .{buffer.content()});
}
const Terminal = struct {
line: usize = 0,
pub fn write(self: *Terminal, text: []const u8) void {
self.line += 1;
std.debug.print("{d}| {s}\n", .{ self.line, text });
}
pub fn clear(self: *Terminal) void {
self.line = 0;
std.debug.print("\x1b[2J\x1b[H", .{});
}
};
const Buffer = struct {
storage: [512]u8 = undefined,
length: usize = 0,
pub fn write(self: *Buffer, text: []const u8) void {
const remaining = self.storage.len - self.length;
const n = @min(remaining, text.len);
@memcpy(self.storage[self.length..][0..n], text[0..n]);
self.length += n;
}
pub fn clear(self: *Buffer) void {
self.length = 0;
}
// It is not in the contract; belongs to this type.
pub fn content(self: *const Buffer) []const u8 {
return self.storage[0..self.length];
}
};
const Writer = struct {
const Contract = struct {
write: *const fn (implementation: *anyopaque, text: []const u8) void,
clear: *const fn (implementation: *anyopaque) void,
};
// Calling virtual table by a namespace
const Vmt = virtual.MethodTable(Contract);
vtable: Vmt,
pub fn init(implementation: anytype) Writer {
return .{ .vtable = Vmt.init(implementation) };
}
pub fn write(self: Writer, text: []const u8) void {
self.vtable.methods.write(self.vtable.implementation, text);
}
pub fn clear(self: Writer) void {
self.vtable.methods.clear(self.vtable.implementation);
}
// Shared by all backends, not enforced by the contract.
pub fn writeFmt(self: Writer, comptime fmt: []const u8, args: anytype) void {
var buf: [256]u8 = undefined;
const text = std.fmt.bufPrint(&buf, fmt, args) catch return;
self.write(text);
}
};
Sözlük
- Arayüz → Interface
- İşlem anı seçimli köpbiçim → Runtime dispatched polymorhism
- İşlev işaretçisi → Function pointer
- Jeneriklik → Genericity
- Çağrı uzlaşımı → Calling convention
- Bildirim → Declaration
- Sarıcı → Wrapper