temporalz/src/abi.zig
2026-01-30 03:01:04 +06:00

508 lines
19 KiB
Zig

const std = @import("std");
pub const c = @cImport({
@cInclude("AnyCalendarKind.h");
@cInclude("Calendar.h");
@cInclude("Duration.h");
@cInclude("ErrorKind.h");
@cInclude("I128Nanoseconds.h");
@cInclude("Instant.h");
@cInclude("OwnedRelativeTo.h");
@cInclude("ParsedDate.h");
@cInclude("ParsedDateTime.h");
@cInclude("ParsedZonedDateTime.h");
@cInclude("PlainDate.h");
@cInclude("PlainDateTime.h");
@cInclude("PlainMonthDay.h");
@cInclude("PlainTime.h");
@cInclude("PlainYearMonth.h");
@cInclude("RelativeTo.h");
@cInclude("TimeZone.h");
@cInclude("ZonedDateTime.h");
});
pub const to_string_rounding_options_auto: c.ToStringRoundingOptions = .{
.precision = .{ .is_minute = false, .precision = toOption(c.OptionU8, null) },
.smallest_unit = toUnitOption(null),
.rounding_mode = toRoundingModeOption(null),
};
const u64_high_bit_mask: u64 = 1 << 63;
/// Covert a Rust `I128Nanoseconds` struct to a Zig `i128`.
///
/// Ported from temporal_rs's [`From<ffi::I128Nanoseconds>`](https://github.com/boa-dev/temporal/blob/89bfca1f5b918d00a19354664e1da11da51305ee/temporal_capi/src/instant.rs#L172-L186) trait for `i128`.
pub fn fromI128Nanoseconds(ns: c.I128Nanoseconds) i128 {
const is_neg = (ns.high & u64_high_bit_mask) != 0;
const ns_high: u128 = @intCast((ns.high & ~u64_high_bit_mask));
const total: i128 = @intCast((ns_high << 64) + ns.low);
return if (is_neg) -total else total;
}
/// Covert a Zig `i128` to a Rust `I128Nanoseconds` struct.
///
/// Ported from temporal_rs's [`From<i128>`](https://github.com/boa-dev/temporal/blob/89bfca1f5b918d00a19354664e1da11da51305ee/temporal_capi/src/instant.rs#L188-L207) trait for `ffi::I128Nanoseconds`.
pub fn toI128Nanoseconds(ns: i128) c.I128Nanoseconds {
std.debug.assert(ns != std.math.minInt(i128));
const is_neg = ns < 0;
const ns_abs = @abs(ns);
const high: u64 = @intCast(ns_abs >> 64);
const low: u64 = @truncate(ns_abs);
return .{ .high = if (is_neg) high | u64_high_bit_mask else high, .low = low };
}
/// Convert a Rust `DiplomatStringView` to a Zig slice.
pub fn fromDiplomatStringView(sv: c.DiplomatStringView) []const u8 {
return sv.data[0..sv.len];
}
/// Convert a Zig slice to a Rust `DiplomatStringView`.
pub fn toDiplomatStringView(s: []const u8) c.DiplomatStringView {
return .{ .data = s.ptr, .len = s.len };
}
/// Convert a Zig slice to a Rust `DiplomatString16View`.
pub fn toDiplomatString16View(s: []const u16) c.DiplomatString16View {
return .{ .data = s.ptr, .len = s.len };
}
/// Convert a Rust `Option<T>` to a Zig `?T`.
pub fn fromOption(value: anytype) ?Success(@TypeOf(value)) {
return success(value);
}
/// Convert a Zig `?T` to a Rust `Option<T>`.
pub fn toOption(comptime T: type, maybe_value: ?Success(T)) T {
return if (maybe_value) |value|
.{ .is_ok = true, .unnamed_0 = .{ .ok = value } }
else
.{ .is_ok = false };
}
/// Convert a Zig `?ArithmeticOverflow` to a Rust `Option<ArithmeticOverflow>`.
pub fn toArithmeticOverflowOption(maybe_value: ?c.RoundingMode) c.ArithmeticOverflow_option {
return toOption(c.ArithmeticOverflow_option, maybe_value);
}
/// Convert a Zig `?Disambiguation` to a Rust `Option<Disambiguation>`.
pub fn toDisambiguationOption(maybe_value: ?c.Disambiguation) c.Disambiguation_option {
return toOption(c.Disambiguation_option, maybe_value);
}
/// Convert a Zig `?OffsetDisambiguation` to a Rust `Option<OffsetDisambiguation>`.
pub fn toOffsetDisambiguationOption(maybe_value: ?c.OffsetDisambiguation) c.OffsetDisambiguation_option {
return toOption(c.OffsetDisambiguation_option, maybe_value);
}
/// Convert a Zig `?PartialDate` to a Rust `Option<PartialDate>`.
pub fn toPartialDateOption(maybe_value: ?c.PartialDate) c.PartialDate_option {
return toOption(c.PartialDate_option, maybe_value);
}
/// Convert a Zig `?RoundingMode` to a Rust `Option<RoundingMode>`.
pub fn toRoundingModeOption(maybe_value: ?c.RoundingMode) c.RoundingMode_option {
return toOption(c.RoundingMode_option, maybe_value);
}
/// Convert a Zig `?TimeZone` to a Rust `Option<TimeZone>`.
pub fn toTimeZoneOption(maybe_value: ?c.TimeZone) c.TimeZone_option {
return toOption(c.TimeZone_option, maybe_value);
}
/// Convert a Zig `?Unit` to a Rust `Option<Unit>`.
pub fn toUnitOption(maybe_value: ?c.Unit) c.Unit_option {
return toOption(c.Unit_option, maybe_value);
}
// Wraps values from a `c.RelativeTo` or `c.OwnedRelativeTo`.
pub const RelativeTo = union(enum) {
none,
owned_plain_date: *c.PlainDate,
owned_zoned_date_time: *c.ZonedDateTime,
borrowed_plain_date: *const c.PlainDate,
borrowed_zoned_date_time: *const c.ZonedDateTime,
pub fn fromOwned(owned: c.OwnedRelativeTo) RelativeTo {
if (owned.date) |plain_date| {
return .{ .owned_plain_date = plain_date };
} else if (owned.zoned) |zoned_date_time| {
return .{ .owned_zoned_date_time = zoned_date_time };
} else {
return .none;
}
}
pub fn toRust(self: RelativeTo) c.RelativeTo {
return switch (self) {
.none => .{ .date = null, .zoned = null },
.owned_plain_date => |plain_date| .{ .date = plain_date, .zoned = null },
.owned_zoned_date_time => |zoned_date_time| .{ .date = null, .zoned = zoned_date_time },
.borrowed_plain_date => |plain_date| .{ .date = plain_date, .zoned = null },
.borrowed_zoned_date_time => |zoned_date_time| .{ .date = null, .zoned = zoned_date_time },
};
}
pub fn deinit(self: RelativeTo) void {
switch (self) {
.owned_plain_date => |plain_date| c.temporal_rs_PlainDate_destroy(plain_date),
.owned_zoned_date_time => |zoned_date_time| c.temporal_rs_ZonedDateTime_destroy(zoned_date_time),
else => {},
}
}
};
pub const DiplomatWrite = struct {
gpa: std.mem.Allocator,
array_list: std.ArrayList(u8),
inner: c.DiplomatWrite,
pub fn init(gpa: std.mem.Allocator) DiplomatWrite {
return .{
.gpa = gpa,
.array_list = .empty,
.inner = .{
// NOTE: We use `@fieldParentPtr()` on the `inner` struct field to get to the other
// fields instead of creating a context externally and storing a pointer.
.context = null,
.buf = undefined,
.len = 0,
.cap = 0,
.grow_failed = false,
.flush = flush,
.grow = grow,
},
};
}
pub fn deinit(self: *DiplomatWrite) void {
self.array_list.deinit(self.gpa);
}
pub fn toOwnedSlice(self: *DiplomatWrite) std.mem.Allocator.Error![]u8 {
if (self.inner.grow_failed) return error.OutOfMemory;
self.inner = undefined; // Invalidate the inner struct to prevent further writes
return self.array_list.toOwnedSlice(self.gpa);
}
fn flush(inner: ?*c.DiplomatWrite) callconv(.c) void {
const self: *DiplomatWrite = @fieldParentPtr("inner", inner.?);
self.array_list.items.len = inner.?.len;
}
fn grow(inner: ?*c.DiplomatWrite, size: usize) callconv(.c) bool {
const self: *DiplomatWrite = @fieldParentPtr("inner", inner.?);
self.array_list.ensureTotalCapacity(self.gpa, size) catch return false;
inner.?.buf = self.array_list.items.ptr;
inner.?.cap = self.array_list.capacity;
return true;
}
};
test DiplomatWrite {
const gpa = std.testing.allocator;
var write = DiplomatWrite.init(gpa);
defer write.deinit();
const WriteImpl = struct {
inner: *c.DiplomatWrite,
// https://github.com/rust-diplomat/diplomat/blob/2b903255187976779798fc89df3fee7298641c80/runtime/src/write.rs#L70-L73
pub fn flush(self: @This()) void {
self.inner.flush.?(self.inner);
}
// https://github.com/rust-diplomat/diplomat/blob/2b903255187976779798fc89df3fee7298641c80/runtime/src/write.rs#L76-L94
pub fn writeStr(self: @This(), s: []const u8) void {
if (self.inner.grow_failed) {
return;
}
const needed_len = self.inner.len + s.len;
if (needed_len > self.inner.cap) {
const success_ = self.inner.grow.?(self.inner, needed_len);
if (!success_) {
self.inner.grow_failed = true;
return;
}
}
std.debug.assert(needed_len <= self.inner.cap);
@memcpy(self.inner.buf[self.inner.len..][0..s.len], s);
self.inner.len = needed_len;
}
};
var write_impl: WriteImpl = .{ .inner = &write.inner };
write_impl.writeStr("Hello World");
write_impl.flush();
try std.testing.expectEqual(write.array_list.items.ptr, write.inner.buf);
try std.testing.expectEqual(write.array_list.items.len, write.inner.len);
try std.testing.expectEqual(write.array_list.capacity, write.inner.cap);
try std.testing.expectEqual(false, write.inner.grow_failed);
try std.testing.expectEqualSlices(u8, "Hello World", write.array_list.items);
const slice = try write.toOwnedSlice();
defer gpa.free(slice);
try std.testing.expectEqualSlices(u8, "Hello World", slice);
try std.testing.expectEqualSlices(u8, &.{}, write.array_list.items);
}
/// Converts a "result" value to its "success" type, or returns `null` if the value is an error.
/// This is `inline` to prevent binary bloat, because each instantiation is expected to be called
/// only once.
pub inline fn success(result: anytype) ?Success(@TypeOf(result)) {
if (!result.is_ok) return null;
if (Success(@TypeOf(result)) == void) return;
return result.unnamed_0.ok;
}
/// Given the C API representation of a `Result<T, E>`, returns the type 'T'.
pub fn Success(comptime Result: type) type {
const Union = @FieldType(Result, "unnamed_0");
if (!@hasField(Union, "ok")) return void;
return @FieldType(Union, "ok");
}
/// Temporal error set mapping to C API error kinds
pub const TemporalError = error{
/// Generic temporal error
Generic,
/// Type error (invalid type or conversion)
TypeError,
/// Range error (value out of valid range)
RangeError,
/// Syntax error (invalid format or parsing)
SyntaxError,
/// Assertion failed (should not happen)
AssertionFailed,
/// Other unspecified error
Unknown,
};
/// Extract result from C API, mapping errors to specific Zig error types.
pub inline fn extractResult(result: anytype) TemporalError!Success(@TypeOf(result)) {
if (success(result)) |value| return value;
// Handle the error - check if the union has an 'err' field
const Union = @FieldType(@TypeOf(result), "unnamed_0");
if (@hasField(Union, "err")) {
const err = result.unnamed_0.err;
// const message = if (fromOption(err.msg)) |sv|
// fromDiplomatStringView(sv)
// else
// "(no error message)";
return switch (err.kind) {
c.ErrorKind_Generic => TemporalError.Generic,
c.ErrorKind_Type => TemporalError.TypeError,
c.ErrorKind_Range => TemporalError.RangeError,
c.ErrorKind_Syntax => TemporalError.SyntaxError,
c.ErrorKind_Assert => @panic("temporal_rs assertion failed"),
else => TemporalError.Unknown,
};
}
// Fallback for result types without detailed error information
return TemporalError.Generic;
}
const t = @import("temporal.zig");
const dur = @import("Duration.zig");
const ins = @import("Instant.zig");
pub const to = struct {
pub fn toArithmeticOverflow(val: anytype) c.ArithmeticOverflow {
return switch (val) {
.constrain => c.ArithmeticOverflow_Constrain,
.reject => c.ArithmeticOverflow_Reject,
};
}
pub fn toShowCalendar(val: anytype) c.ShowCalendar {
return switch (val) {
.auto => c.ShowCalendar_Auto,
.always => c.ShowCalendar_Always,
.never => c.ShowCalendar_Never,
.critical => c.ShowCalendar_Critical,
};
}
pub fn toTimeZone(val: anytype) c.TimeZone {
return val._inner;
}
pub fn toDisambiguation(d: anytype) c.Disambiguation {
return switch (d) {
.compatible => c.Disambiguation_Compatible,
.earlier => c.Disambiguation_Earlier,
.later => c.Disambiguation_Later,
.reject => c.Disambiguation_Reject,
};
}
pub fn toOffsetDisambiguation(o: anytype) c.OffsetDisambiguation {
return switch (o) {
.use_offset => c.OffsetDisambiguation_Use,
.prefer_offset => c.OffsetDisambiguation_Prefer,
.ignore_offset => c.OffsetDisambiguation_Ignore,
.reject => c.OffsetDisambiguation_Reject,
};
}
pub fn calendarDisplay(cd: anytype) c.DisplayCalendar {
return switch (cd) {
.auto => c.DisplayCalendar_Auto,
.always => c.DisplayCalendar_Always,
.never => c.DisplayCalendar_Never,
.critical => c.DisplayCalendar_Critical,
};
}
pub fn displayOffset(o: anytype) c.DisplayOffset {
return switch (o) {
.auto => c.DisplayOffset_Auto,
.never => c.DisplayOffset_Never,
};
}
pub fn toDisplayTimeZone(val: anytype) c.DisplayTimeZone {
return switch (val) {
.auto => c.DisplayTimeZone_Auto,
.never => c.DisplayTimeZone_Never,
.critical => c.DisplayTimeZone_Critical,
};
}
pub fn unit(opt: ?t.Unit) ?c.Unit {
return if (opt) |u| @as(c.Unit, @intCast(to.unitToCApi(u))) else null;
}
fn unitToCApi(u: t.Unit) c_uint {
return switch (u) {
.auto => c.Unit_Auto,
.nanosecond => c.Unit_Nanosecond,
.microsecond => c.Unit_Microsecond,
.millisecond => c.Unit_Millisecond,
.second => c.Unit_Second,
.minute => c.Unit_Minute,
.hour => c.Unit_Hour,
.day => c.Unit_Day,
.week => c.Unit_Week,
.month => c.Unit_Month,
.year => c.Unit_Year,
};
}
pub fn roundingMode(opt: ?t.RoundingMode) ?c.RoundingMode {
return if (opt) |m| @as(c.RoundingMode, @intCast(to.roundingModeToCApi(m))) else null;
}
fn roundingModeToCApi(m: t.RoundingMode) c_uint {
return switch (m) {
.ceil => c.RoundingMode_Ceil,
.floor => c.RoundingMode_Floor,
.expand => c.RoundingMode_Expand,
.trunc => c.RoundingMode_Trunc,
.half_ceil => c.RoundingMode_HalfCeil,
.half_floor => c.RoundingMode_HalfFloor,
.half_expand => c.RoundingMode_HalfExpand,
.half_trunc => c.RoundingMode_HalfTrunc,
.half_even => c.RoundingMode_HalfEven,
};
}
pub fn sign(opt: ?t.Sign) ?c.Sign {
return if (opt) |s| @as(c.Sign, @intCast(to.signToCApi(s))) else null;
}
fn signToCApi(s: t.Sign) c_int {
return switch (s) {
.positive => c.Sign_Positive,
.zero => c.Sign_Zero,
.negative => c.Sign_Negative,
};
}
pub fn strRoundingOpts(self: t.ToStringRoundingOptions) c.ToStringRoundingOptions {
const precision: c.Precision = if (self.fractional_second_digits) |fsd|
.{ .is_minute = false, .precision = toOption(c.OptionU8, fsd) }
else if (self.smallest_unit) |su|
switch (su) {
.second => .{ .is_minute = false, .precision = toOption(c.OptionU8, 0) },
.millisecond => .{ .is_minute = false, .precision = toOption(c.OptionU8, 3) },
.microsecond => .{ .is_minute = false, .precision = toOption(c.OptionU8, 6) },
.nanosecond => .{ .is_minute = false, .precision = toOption(c.OptionU8, 9) },
else => .{ .is_minute = false, .precision = toOption(c.OptionU8, null) },
}
else
.{ .is_minute = false, .precision = toOption(c.OptionU8, null) };
return .{
.precision = precision,
.smallest_unit = toUnitOption(unit(self.smallest_unit)),
.rounding_mode = toRoundingModeOption(roundingMode(self.rounding_mode)),
};
}
pub fn durRoundingOpts(self: dur.RoundingOptions) c.RoundingOptions {
return .{
.largest_unit = toUnitOption(unit(self.largest_unit)),
.smallest_unit = toUnitOption(unit(self.smallest_unit)),
.rounding_mode = toRoundingModeOption(roundingMode(self.rounding_mode)),
.increment = toOption(c.OptionU32, self.rounding_increment),
};
}
pub fn tz(self: ins.TimeZone) c.TimeZone {
return self._inner;
}
pub fn partialdur(self: dur.PartialDuration) c.PartialDuration {
return .{
.years = toOption(c.OptionI64, self.years),
.months = toOption(c.OptionI64, self.months),
.weeks = toOption(c.OptionI64, self.weeks),
.days = toOption(c.OptionI64, self.days),
.hours = toOption(c.OptionI64, self.hours),
.minutes = toOption(c.OptionI64, self.minutes),
.seconds = toOption(c.OptionI64, self.seconds),
.milliseconds = toOption(c.OptionI64, self.milliseconds),
.microseconds = toOption(c.OptionF64, self.microseconds),
.nanoseconds = toOption(c.OptionF64, self.nanoseconds),
};
}
pub fn durRelativeTo(self: dur.RelativeTo) c.RelativeTo {
switch (self) {
.plain_date => |pd| return .{ .date = pd._inner },
.zoned_date_time => |zdt| return .{ .zoned = zdt._inner },
.plain_date_time => |pdt| return .{ .date = (pdt.toPlainDate() catch unreachable)._inner },
}
}
pub fn roundingOpts(self: t.RoundingOptions) c.RoundingOptions {
return .{
.largest_unit = toUnitOption(to.unit(self.largest_unit)),
.smallest_unit = toUnitOption(to.unit(self.smallest_unit)),
.rounding_mode = toRoundingModeOption(to.roundingMode(self.rounding_mode)),
.increment = toOption(c.OptionU32, self.rounding_increment),
};
}
pub fn diffsettings(self: t.DifferenceSettings) c.DifferenceSettings {
return .{
.largest_unit = toUnitOption(to.unit(self.largest_unit)),
.smallest_unit = toUnitOption(to.unit(self.smallest_unit)),
.rounding_mode = toRoundingModeOption(to.roundingMode(self.rounding_mode)),
.increment = toOption(c.OptionU32, self.rounding_increment),
};
}
};
pub const from = struct {
pub fn sign(value: c_int) t.Sign {
return switch (value) {
c.Sign_Positive => .positive,
c.Sign_Zero => .zero,
c.Sign_Negative => .negative,
else => .zero,
};
}
};