Dealing with external symbol references in a multi-crate workspace (napi-rs)

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_binmy_lib
my_napi_pkgmy_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_binmy_lib(features=[])
my_napi_pkgmy_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_binmy_lib(features=["napi"])
my_napi_pkgmy_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_binmy_lib
my_napi_pkgmy_napi_libmy_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:

  1. Create a proxy struct in the my_napi_lib crate that I use use the napi_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 that my_lib::MyStruct and my_napi_lib::MyStruct have the same layout.

  2. 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 from my_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?
  • 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! :pray:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.