Apologies in advance for the wall of text, this problem is a little beyond me.
The Problem
I'm working on a project that uses napi-rs
, a library for linking rust libraries against node, and provides a macro to automatically generate the glue code to translate node objects types into rust-native types. Those macros have to be placed on the struct definitions:
use napi_derive::napi;
#[napi]
struct MyStruct {
pub my_field: u64,
}
This glue code is unsafe
, under-documented, and cumbersome to write manually for large structs, so the macros are pretty essential to using the library.
This project is in a multi-crate workspace that builds both libraries that link to node, and others build standalone binaries. If a reference to libnode
is included in a standalone binary's build, it will fail to link.
Say I have the following package structure:
my_bin
→ my_lib
my_napi_pkg
→ my_lib
I've been working around this issue so far by adding a napi
feature to my_lib
, and making all the #[napi]
macros conditional on the feature with cfg_if
.
my_bin
→ my_lib(features=[])
my_napi_pkg
→ my_lib(features=["napi"])
This works while building a single package. However, building the whole workspace fails because during feature unification, my_lib
is always built with the napi
feature:
my_bin
→ my_lib(features=["napi"])
my_napi_pkg
→ my_lib(features=["napi"])
That causes my_bin
to fail to link, since it contains unresolved references to external symbols from node. my_napi_pkg
will compile, because it has a build script that sets up those references.
The Solutions I am Thinking of:
Because mutually exclusive features aren't supported, I am thinking the only way to fix my problem is to restructure my crates like this:
my_bin
→ my_lib
my_napi_pkg
→ my_napi_lib
→ my_lib
where my_napi_lib
implements the conversion to/from napi types.
Because of orphan impl rules, I can't implement the napi traits directly on the types from my_lib
in my_napi_lib
.
Normally I'd want to use a newtype wrapper around the type and implement From/Into on it, but then I couldn't use the napi_derive
macros to generate an implementation for me.
I think that means I have two viable options for what to put in my_napi_lib
:
-
Create a proxy struct in the
my_napi_lib
crate that I use use thenapi_derive
macros on, and manually implement from/into on the struct.struct MyNapiStruct { pub my_field: u64; // ... continue for a dozen or so fields }; impl Into<my_lib::MyStruct> for MyNapiStruct { // ... etc
This would incur some runtime cost for moving each field between the structs in the
Into
impl, because I am not guaranteed thatmy_lib::MyStruct
andmy_napi_lib::MyStruct
have the same layout. -
Use a newtype wrapper, and manually implement the napi traits that are normally derived (or write my own macro to do so).
struct MyNapiStruct(my_lib::MyStruct);
Because the struct contains the literal other type from
my_lib
converting into the type frommy_lib
would just pass ownership of the field to the receiver and not incur additional runtime cost.
My Questions
- Am I understanding the runtime overhead of solutions (1.) and (2.) correctly?
- Will 2 identical structs tend to have the same layout if built with the same settings? And can the compiler optimize the move in that case?
- Is there a better / more obvious solution that I'm missing?
- Since the
napi
code is never used in the binaries, could I link against node during linking, then then remove the relevant code from the output binary so it can run on a system without node?
- Since the
- Is the runtime cost of moving ownership between the 2 different structs in solution (1.) trivial, and I'm wasting my time worrying about it?
Sorry for the wall of text, and thanks for any help!