Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/basic/src/class.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const napi = @import("napi");

const Test = struct {
name: []u8,
age: i32,
};

pub const TestClass = napi.Class(Test);
3 changes: 3 additions & 0 deletions examples/basic/src/hello.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const array = @import("array.zig");
const object = @import("object.zig");
const function = @import("function.zig");
const thread_safe_function = @import("thread_safe_function.zig");
const class = @import("class.zig");
const log = @import("log/log.zig");

pub const test_i32 = number.test_i32;
Expand Down Expand Up @@ -38,6 +39,8 @@ pub const create_function = function.create_function;

pub const call_thread_safe_function = thread_safe_function.call_thread_safe_function;

pub const TestClass = class.TestClass;

pub const test_hilog = log.test_hilog;

comptime {
Expand Down
3 changes: 3 additions & 0 deletions src/napi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const module = @import("./prelude/module.zig");
const worker = @import("./napi/wrapper/worker.zig");
const err = @import("./napi/wrapper/error.zig");
const thread_safe_function = @import("./napi/wrapper/thread_safe_function.zig");
const class = @import("./napi/wrapper/class.zig");

pub const napi_sys = @import("napi-sys");

Expand All @@ -30,6 +31,8 @@ pub const Function = function.Function;
pub const CallbackInfo = callback_info.CallbackInfo;
pub const Worker = worker.Worker;
pub const ThreadSafeFunction = thread_safe_function.ThreadSafeFunction;
pub const Class = class.Class;
pub const ClassWithoutInit = class.ClassWithoutInit;

pub const NODE_API_MODULE = module.NODE_API_MODULE;
pub const NODE_API_MODULE_WITH_INIT = module.NODE_API_MODULE_WITH_INIT;
31 changes: 31 additions & 0 deletions src/napi/util/allocator.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const std = @import("std");

pub const AllocatorManager = struct {
allocator: std.mem.Allocator,

const Self = @This();

pub fn init() Self {
return Self{
.allocator = std.heap.page_allocator,
};
}

pub fn get(self: *const Self) std.mem.Allocator {
return self.allocator;
}

pub fn set(self: *Self, new_allocator: std.mem.Allocator) void {
self.allocator = new_allocator;
}
};

pub const global_manager = AllocatorManager.init();

pub fn globalAllocator() std.mem.Allocator {
return global_manager.get();
}

pub fn setGlobalAllocator(new_allocator: std.mem.Allocator) void {
global_manager.set(new_allocator);
}
5 changes: 5 additions & 0 deletions src/napi/util/helper.zig
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,8 @@ pub fn collectFunctionArgs(comptime functions: anytype) type {
.is_tuple = true,
} });
}

pub fn shortTypeName(comptime T: type) []const u8 {
var iter = std.mem.splitBackwardsScalar(u8, @typeName(T), '.');
return iter.first();
}
6 changes: 5 additions & 1 deletion src/napi/util/napi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Env = @import("../env.zig").Env;
const NapiError = @import("../wrapper/error.zig");
const Function = @import("../value/function.zig").Function;
const ThreadSafeFunction = @import("../wrapper/thread_safe_function.zig").ThreadSafeFunction;
const class = @import("../wrapper/class.zig");

pub const Napi = struct {
pub fn from_napi_value(env: napi.napi_env, raw: napi.napi_value, comptime T: type) T {
Expand Down Expand Up @@ -202,6 +203,7 @@ pub const Napi = struct {
const array = try NapiValue.Array.New(Env.from_raw(env), value);
return array.raw;
}

const object = try NapiValue.Object.New(Env.from_raw(env), value);
return object.raw;
},
Expand All @@ -227,7 +229,9 @@ pub const Napi = struct {
return NapiValue.String.New(Env.from_raw(env), value).raw;
},
else => {

if (comptime class.isClass(value)) {
return try value.to_napi_value(Env.from_raw(env));
}
// TODO: Implement this
@compileError("Unsupported type: " ++ @typeName(value));
},
Expand Down
10 changes: 9 additions & 1 deletion src/napi/wrapper/callback_info.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub const CallbackInfo = struct {
raw: napi.napi_callback_info,
env: napi.napi_env,
args: []const value.NapiValue,
this: napi.napi_value,

pub fn from_raw(env: napi.napi_env, raw: napi.napi_callback_info) CallbackInfo {
var init_argc: usize = 0;
Expand All @@ -19,7 +20,9 @@ pub const CallbackInfo = struct {
const args_raw = allocator.alloc(napi.napi_value, init_argc) catch @panic("OOM");
defer allocator.free(args_raw);

const status2 = napi.napi_get_cb_info(env, raw, &init_argc, args_raw.ptr, null, null);
var this: napi.napi_value = undefined;

const status2 = napi.napi_get_cb_info(env, raw, &init_argc, args_raw.ptr, &this, null);
if (status2 != napi.napi_ok) {
@panic("Failed to get callback info");
}
Expand All @@ -34,6 +37,7 @@ pub const CallbackInfo = struct {
.raw = raw,
.env = env,
.args = result,
.this = this,
};
}

Expand All @@ -44,4 +48,8 @@ pub const CallbackInfo = struct {
pub fn Get(self: CallbackInfo, index: usize) value.NapiValue {
return self.args[index];
}

pub fn This(self: CallbackInfo) napi.napi_value {
return self.this;
}
};
225 changes: 225 additions & 0 deletions src/napi/wrapper/class.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
const std = @import("std");
const napi = @import("napi-sys").napi_sys;
const CallbackInfo = @import("./callback_info.zig").CallbackInfo;
const napi_env = @import("../env.zig");
const Napi = @import("../util/napi.zig").Napi;
const helper = @import("../util/helper.zig");
const GlobalAllocator = @import("../util/allocator.zig");

var class_constructors: std.StringHashMap(napi.napi_value) = std.StringHashMap(napi.napi_value).init(GlobalAllocator.globalAllocator());

pub fn ClassWrapper(comptime T: type, comptime HasInit: bool) type {
const type_info = @typeInfo(T);
if (type_info != .@"struct") {
@compileError("Class() only support struct type");
}

if (type_info.@"struct".is_tuple) {
@compileError("Class() does not support tuple type");
}

const fields = type_info.@"struct".fields;
const decls = type_info.@"struct".decls;

const class_name = comptime helper.shortTypeName(T);
return struct {
const WrappedType = T;

env: napi.napi_env,
raw: napi.napi_value,

const Self = @This();

// 构造器回调
fn constructor_callback(env: napi.napi_env, callback_info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const infos = CallbackInfo.from_raw(env, callback_info);

const data = GlobalAllocator.globalAllocator().create(T) catch return null;

if (@hasDecl(T, "init")) {
var tuple_args: std.meta.ArgsTuple(T.init) = undefined;
inline for (@typeInfo(std.meta.ArgsTuple(T.init)).@"fn".params, 0..) |arg, i| {
tuple_args[i] = Napi.from_napi_value(infos.env, infos.args[i].raw, arg.type.?);
}

data.* = @call(.auto, T.init, tuple_args) catch {
GlobalAllocator.globalAllocator().destroy(data);
return null;
};
} else {
// init with zero
data.* = std.mem.zeroes(T);
// if HasInit, init with args with order of fields
if (comptime HasInit) {
inline for (fields, 0..) |field, i| {
@field(data.*, field.name) = Napi.from_napi_value(infos.env, infos.args[i].raw, field.type);
}
}
}

const this_obj = infos.This();

var ref: napi.napi_ref = undefined;
const status = napi.napi_wrap(env, this_obj, data, finalize_callback, null, &ref);
if (status != napi.napi_ok) {
GlobalAllocator.globalAllocator().destroy(data);
return null;
}

var ref_count: u32 = undefined;
_ = napi.napi_reference_unref(env, ref, &ref_count);

return this_obj;
}

fn finalize_callback(env: napi.napi_env, data: ?*anyopaque, hint: ?*anyopaque) callconv(.c) void {
_ = env;
_ = hint;

if (data) |ptr| {
const typed_data: *T = @ptrCast(@alignCast(ptr));
if (@hasDecl(T, "deinit")) {
typed_data.deinit();
}
GlobalAllocator.globalAllocator().destroy(typed_data);
}
}

fn define_class(env: napi.napi_env) !napi.napi_value {
comptime var property_count: usize = 0;
inline for (fields) |_| {
property_count += 1;
}
inline for (decls) |decl| {
if (@typeInfo(@TypeOf(@field(T, decl.name))) == .Fn and
!std.mem.eql(u8, decl.name, "init") and
!std.mem.eql(u8, decl.name, "deinit"))
{
property_count += 1;
}
}

var properties: [property_count]napi.napi_property_descriptor = undefined;
var prop_idx: usize = 0;

inline for (fields) |field| {
const FieldAccessor = struct {
fn getter(getter_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const cb_info = CallbackInfo.from_raw(getter_env, info);
var data: ?*anyopaque = null;
_ = napi.napi_unwrap(getter_env, cb_info.This(), &data);
if (data == null) return null;

const instance: *T = @ptrCast(@alignCast(data.?));
const field_value = @field(instance.*, field.name);
return Napi.to_napi_value(getter_env, field_value, field.name) catch null;
}

fn setter(setter_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const cb_info = CallbackInfo.from_raw(setter_env, info);
var data: ?*anyopaque = null;
_ = napi.napi_unwrap(setter_env, cb_info.This(), &data);
if (data == null) return null;

const instance: *T = @ptrCast(@alignCast(data.?));
const args = cb_info.args;
if (args.len > 0) {
const new_value = Napi.from_napi_value(setter_env, args[0].raw, field.type);
@field(instance.*, field.name) = new_value;
}

return null;
}
};

properties[prop_idx] = napi.napi_property_descriptor{
.utf8name = @ptrCast(field.name.ptr),
.name = null,
.method = null,
.getter = FieldAccessor.getter,
.setter = FieldAccessor.setter,
.value = null,
.attributes = napi.napi_default,
.data = null,
};
prop_idx += 1;
}

inline for (decls) |decl| {
if (@typeInfo(@TypeOf(@field(T, decl.name))) == .Fn and
!std.mem.eql(u8, decl.name, "init") and
!std.mem.eql(u8, decl.name, "deinit"))
{
const method = @field(T, decl.name);
const method_info = @typeInfo(@TypeOf(method));
const params = method_info.Fn.params;
const is_instance_method = params.len > 0 and params[0].type.? == *T;

const MethodWrapper = struct {
fn call(method_env: napi.napi_env, info: napi.napi_callback_info) callconv(.c) napi.napi_value {
const cb_info = CallbackInfo.from_raw(method_env, info);

if (is_instance_method) {
var data: ?*anyopaque = null;
_ = napi.napi_unwrap(method_env, cb_info.This(), &data);
if (data == null) return null;

const instance: *T = @ptrCast(@alignCast(data.?));
const result = method(instance);
return Napi.to_napi_value(method_env, result, decl.name) catch null;
} else {
const result = method();
return Napi.to_napi_value(method_env, result, decl.name) catch null;
}
}
};

properties[prop_idx] = napi.napi_property_descriptor{
.utf8name = @ptrCast(decl.name.ptr),
.name = null,
.method = MethodWrapper.call,
.getter = null,
.setter = null,
.value = null,
.attributes = if (is_instance_method) napi.napi_default else napi.napi_static,
.data = null,
};
prop_idx += 1;
}
}

var constructor: napi.napi_value = undefined;
_ = napi.napi_define_class(env, class_name.ptr, class_name.len, constructor_callback, null, prop_idx, &properties, &constructor);

try class_constructors.put(class_name, constructor);
return constructor;
}

fn define_custom_method(_: napi.napi_env, _: napi.napi_value) !void {}

/// to_napi_value will create a class constructor and return it
pub fn to_napi_value(env: napi_env.Env) !napi.napi_value {
const constructor = try Self.define_class(env.raw);

try Self.define_custom_method(env.raw, constructor);

return constructor;
}
};
}

/// Create a class with default constructor function
pub fn Class(comptime T: type) type {
return ClassWrapper(T, true);
}

/// Create a class without default constructor function
pub fn ClassWithoutInit(comptime T: type) type {
return ClassWrapper(T, false);
}

pub fn isClass(T: anytype) bool {
const type_name = @typeName(T);

return std.mem.indexOf(u8, type_name, "ClassWrapper") != null;
}