Shared library constructors in Swift

I'm trying to build a Node.js module in Swift, using their C-compatible N-API. I've gotten the compiling and linking working with a C file and a Swift file, and the next step is now to move everything from C into Swift, one piece at a time.

Full conversion process

This is the C file I'm starting out with:

#include <node_api.h>

napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  return world;
}

napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = { "hello", 0, Method, 0, 0, 0, napi_default, 0 };
  status = napi_define_properties(env, exports, 1, &desc);
  return exports;
}

static napi_module _module = { 1, 0, __FILE__, Init, "hello", NULL, NULL, NULL, NULL };
static void _register_hello(void) __attribute__((constructor));
static void _register_hello(void) {
  napi_module_register(&_module);
}

Trying this out will return the string "world":

$ node -p "require('./build/Release/hello.node').hello()"
world

I started by creating a bridging header in order to bring in node_api.h into Swift:

#define NODE_GYP_MODULE_NAME hello
#define USING_UV_SHARED 1
#define USING_V8_SHARED 1
#define V8_DEPRECATION_WARNINGS 1
#define _DARWIN_USE_64_BIT_INODE 1
#define _LARGEFILE_SOURCE
#define _FILE_OFFSET_BITS 64
#define BUILDING_NODE_EXTENSIO

#include "node_api.h"

The next step was to port the first function, Method, from C to Swift:

+napi_value Method(napi_env env, napi_callback_info info);
-napi_value Method(napi_env env, napi_callback_info info) {
-  napi_status status;
-  napi_value world;
-  status = napi_create_string_utf8(env, "world", 5, &world);
-  return world;
-}
+@_cdecl("Method")
+func method(_ env: napi_env?, _ info: napi_callback_info?) -> napi_value? {
+  var status: napi_status?
+  var world: napi_value?
+  status = napi_create_string_utf8(env, "Swift", 5, &world)
+  return world
+}

I also replaced the string "world" with "Swift" so that we can see that it worked:

$ node -p "require('./build/Release/hello.node').hello()"
Swift

Next, we'll move over the Init function:

+napi_value Init(napi_env env, napi_value exports);
-napi_value Init(napi_env env, napi_value exports) {
-  napi_status status;
-  napi_property_descriptor desc = { "hello", 0, Method, 0, 0, 0, napi_default, 0 };
-  status = napi_define_properties(env, exports, 1, &desc);
-  return exports;
-}
+@_cdecl("Init")
+func initModule(_ env: napi_env?, _ exports: napi_value?) -> napi_value? {
+  var status: napi_status?
+  var desc = napi_property_descriptor(utf8name: "hello", name: nil, method: method, getter: nil, +setter: nil, value: nil, attributes: napi_default, data: nil)
+  status = napi_define_properties(env, exports, 1, &desc)
+  return exports
+}

So far so good, now we just need to port the small shared library constructor, that registers the Node.js module to the process that loaded us.

Everything is working, except for the small part that registers the Node.js module with the process that loaded us. It looks like this in C:

static napi_module _module = { 1, 0, __FILE__, Init, "hello", NULL, { NULL, NULL, NULL, NULL } };

static void _register_hello(void) __attribute__((constructor));
static void _register_hello(void) {
  napi_module_register(&_module);
}

and this is as far as I've been able to take it in Swift:

fileprivate var _module = napi_module(nm_version: NAPI_MODULE_VERSION, nm_flags: 0, nm_filename: #file, nm_register_func: initModule, nm_modname: "hello", nm_priv: nil, reserved: (nil, nil, nil, nil))

@_cdecl("_register_hello")
func _register_hello() -> Void {
  napi_module_register(&_module)
}

While this compiles, it doesn't call napi_module_register when it's loaded, since there is no code being run when I load it. I cannot figure out how to mark the _register_hello function as being the shared library constructor :thinking:

Any help would be much appreciated! :heart:

(for anyone interested in writing Node.js addons in Swift, I'll post a full writeup and repo once I get everything working properly!)

tl;dr is there an equivalent to __attribute__ ((constructor)) in Swift?

2 Likes

No, but you could link in a small C file that registers your entry point as a static constructor.

Hmm, that's unfortunate for me, it would be so clean if I could skip all the C files and go pure Swift! Is this a conscious decision to not support it, or could I submit a feature request for this? :relaxed:


I'm also running into some problems when trying to register the module from Swift. I have the following small C file:

void _register_hello(void);
static void _register_hello_bridge(void) __attribute__((constructor));
static void _register_hello_bridge(void) {
  _register_hello();
}

and I've added this to my Swift file:

var _module = napi_module(nm_version: NAPI_MODULE_VERSION, nm_flags: 0, nm_filename: #file, nm_register_func: initModule, nm_modname: "hello", nm_priv: nil, reserved: (nil, nil, nil, nil))

@_cdecl("_register_hello")
func _register_hello() -> Void {
  print("THIS IS BEING CALLED!")
  napi_module_register(&_module)
}

But for some reason, Node.js thinks that nm_register_func was null and dies with "Module has no declared entry point.".

Moving the _module declaration into _register_hello makes the entire process die with a bus error, so something is definitely not working properly :grin:

Swift strongly favors lazy initialization for everything, because static constructors have issues with order of execution, negative impact on load time, and brittleness when multiple libraries want their constructors to run before everyone else's. As such, we've resisted adding direct support for it to Swift. If you're interacting with a system that relies on static constructors, though, that's a legitimate reason to use one.

It also sounds like napi_module_register expects to receive a persistent pointer that Node can arbitrarily read from after the module has been registered, which will be at odds with how Swift manages variables. When you pass a pointer to a variable from Swift it expects that it will only be used for the duration of the immediate call. You could try declaring your napi_module struct as a global variable in your C stub as well so that it isn't constrained by Swift's semantics, making initModule be your @_cdecl hook that Node then calls into.

2 Likes

Yeah, it seems like the problem with declaring, and assigning, the variable in the global scope didn't run the initializer. I'm guessing that this might be how it works when it's a shared library since nothing gets run as the library constructor, and thus nothing can actually assign the variable :thinking:

Also, when I declared the variable inside of the function, it was (of course :grin:) freed at the end of the function. What worked was declaring the variable global and assigning it inside of the function:

var _module: napi_module?

@_cdecl("_register_hello")
func _register_hello() -> Void {
  _module = napi_module(nm_version: NAPI_MODULE_VERSION, nm_flags: 0, nm_filename: #file, nm_register_func: initModule, nm_modname: "hello", nm_priv: nil, reserved: (nil, nil, nil, nil))
  napi_module_register(&_module!)
}

Do you see any problem with this approach?


Definitively agree with these points, but in this case, I don't think that there is a choice. Node.js expects every module to self-register when it's loaded, and throws an error if they don't.

I would be very happy it there could be an @_constructor that I could put on top of my function :relaxed:

Also, thanks for all the help, really appreciate it!

Passing any Swift variable by pointer so that napi_module_register can capture the pointer is still formally violating Swift's model for property access. I think defining the module structure in a C stub alongside your static constructor would still be the most robust thing to do.

1 Like

I see, maybe it would be possible to do it with Unamanged somehow? :thinking:

One of the problems I'm having with moving it to the C code is that I haven't found a way to import the C headers from my SPM dependency. Basically, what I've done is extract out the header files from Node.js and put in a separate SPM package, so that I can use it from multiple modules.

This works great on the Swift side, I just have to do import NAPI. But I haven't figured out how to import that header from the C target in my package :thinking:

Basically, this is how my setup looks:

NAPI/
  include/
    NAPI.h         # Includes the rest of the header file
    *.h            # Public headers copied from Node.js
  Package.swift    # Has a single target and a single library product
  source.c         # Empty file 

Foobar/
  Sources/
    Foobar/
      main.swift            # Actual implementation
    FoobarTrampoline/
      include/
        FoobarTrampoline.h  # Empty file
      trampoline.c          # Contains my small C code
  Package.swift             # Two targets, and dependency on NAPI

So I haven't figured out how to include NAPI.h from trampoline.c :relaxed:

@Aciid might be able to help you with that.

1 Like

#include <NAPI.h> should work as long as FoobarTrampoline depends on the product of NAPI package.

I'm an idiot, I'd missed the very vital part of having it in the dependencies for that target :man_facepalming:

Thanks!

@LinusU that is way too funny: Google brought me here with the same question when also trying to implement a Nodejs callback into Swift with zero C.

If you want a "truly innovative way" to accomplish it with zero C while also making @Joe_Groff facepalm, you can always use a masm directive to stick your C _register_hello function into the mod_init_func MachO section which will get around the lack of __attribute__((constructor))

So in your init.s file (assuming 64 bit only)...

.section    __DATA,__mod_init_func,mod_init_funcs
.p2align    3
.quad    __register_hello

Note that I am going after your C _register_hello (adding another underscore) declaration that you made via @_cdecl and not the mangled Swift name, which could change from Swift version to version.

Cheers!

That still won't get around the problems with escaping a pointer if you try to store your napi_module information in a Swift global variable.

Terms of Service

Privacy Policy

Cookie Policy