Extern "C" and Wasm with C dependency

I’m working on an environment that only runs wasm32-unknown-unknown and things have been going well for some time.

I’m not doing anything with JavaScript, and I’m not using wasm-pack, wasm-bindgen, or anything like that.

I recently tried to use a dependency that depends on ring for public key verification. This brought up a few issues and questions.

ring has some C dependencies (among other things) and I’ve learned about issues with linking C when targeting wasm32-unknown-unknown due to differences in the ABI so I also tried a pure* Rust version, but these ABI concerns are perhaps less relevant to my issue/question.

The Problem

It appears that ring’s use of extern "C" for calling C functions is clashing with the expected use of extern "C" when targeting Wasm, which results in the Wasm module expecting something to be provided by the environment that is already part of the binary.

I’m hoping I can somehow address without major changes.

Information and observations below. Thanks in advance for any advice and suggestions.

ring 0.16.20

Everything builds successfully but my environment complains that Module imports function 'LIMBS_are_zero' from 'env' that is not exported by the runtime.

LIMBS_are_zero originates here:

and as far as I can tell, the relevant C files are included in the build:

I noticed that the example in the docs on extern includes #[link(name = "my_c_library")].

That seems relevant for the version with C dependencies but not the version below.

ring-xous

Out of curiousity I tried replacing ring with a modified version of ring-xous, since that provides a pure Rust implementation via c2rust.

I still have reservations about this approach because it exchanges concerns about ABI stability compatibility with concerns about security, but for now it eliminates the C dependencies.

The same declarations are still there:

and the implementations are provided like this:

However, I still get the same Module imports function 'LIMBS_are_zero' from 'env' that is not exported by the runtime. error.

I’m assuming that I could fork this and provide/use the rust implementations more directly (without the use of extern "C") but is that necessary?

I'm trying this and it looks promising (I got past the reported error and onto another similar one for different extern "C" declaration) but there appear to be a lot of changes involved.

Removing extern "C" from function implementations apparently means I need to perform explicit conversions that weren't required before.

I'm surprised this happens.

One trick I've used in the past to introduce a "seam" for dependency injection is to declare some extern "Rust" functions that are used in a shared crate high up in the dependency tree, but only provide their #[no_mangle] extern "Rust" implementations in the leaf crate that is being compiled to WebAssembly. Effectively leaving the linker to wire up dependency injection for me.

This always worked correctly every time, and the linker was always able to see that the symbols for __proc_block_metadata() and friends being used by the shared crate were being provided by the leaf crate.

From the outside, my setup should look pretty much identical to what ring is doing, except my code was pure Rust and using extern "Rust", while ring is using extern "C" and a WebAssembly object file that came from a C compiler.

Maybe the linker is somehow not "seeing" the ring-xous library as something that can provide your symbols, so it assumes they'll be provided by the runtime (env)? I dunno, it's weird...

The #[link(...)] attribute explicitly tells the compiler/linker "these symbols will be provided by my_c_library" instead of the default behaviour of looking for the symbols in all the libraries you are linking with.

Maybe have a play around and see if that fixes the "seeing" issue I mentioned?

I'd be careful with this. Both the caller and the callee need to use the same calling convention, so if you only removed the extern "C" from one side you might have set yourself up for a bad time.

You might get away with most simple functions because WebAssembly is more "strongly typed" than x86 assembly (arguments are explicitly specified in a function call instead of making implicit assumptions about the stack's state, etc.), but more complex functions could open you up to some weird errors when loading the WebAssembly module.

2 Likes

That makes sense for the version with the C dependency but gets me into ABI compatibility issues. I can try it anyway and see if that addresses the problem at hand.

For ring-xous there's no C dependency so I'm not sure what I would put there. I assume they kept using extern C on both sides to keep the changes minimal.

I think I have a path forward that probably involves a macro and feature flag for whether to use extern "C" or not.

Yes, I did that on both sides but I appreciate you pointing it out.

When I did that I had to address type errors for things like "expected u32, found usize", and where an integer was previously being coerced into an enum value.

Looking into this: External blocks - The Rust Reference

I suspect that fundamentally this is an issue with extern "C" being intended for importing host functionality when targeting Wasm, and there may be no way around that other than to have a version which doesn't use extern "C".

extern "C" { ... } only imports from the host if there is no other object file linked into the wasm module that defines it. The standard library depends on this as it uses extern "C" { ... } in a couple of places where the definition is part of another crate in the standard library.

2 Likes

Looks like you need to enable the wasm32_c feature to prevent ring from skipping compilation of C code for wasm:

That’s excellent news!

I suppose now I should try to understand why the implemented functions are missing.

I tried that before and still had the same issue. For both ring and ring-xous, extern "C" always tries to import from the host.

I currently have a subset of ring-xous working by removing extern "C" from various places, but it seems like that should be unnecessary based on what you said above.

Perhaps the issue is the mismatched type signature. I’ll look into that.

Do you also get the mismatched type signatures if you compile for wasm32-wasi? (probably need to add wasm32-wasi to the target list in ring's build script for that to work) Wasm32-unknown-unknown isn't abi compatible with C due to a historic accident (someone forgot to implement the calling convention, so it was falling back to whatever LLVM decided, which doesn't match the official C abi for wasm) and we can't change it because wasm-bindgen depends on the wrong calling convention. All other wasm targets however do use the correct C calling convention.

This is at least somewhat excusable in that -unknown-unknown presumes nothing about the environment, so at least by the letter of the law it doesn't have to interoperate with the "platform C ABI," because it doesn't exist.

If/when interface types / the component model (which provides a canonical ABI) are officially adopted, perhaps we can get a new wasm32-canon (or wasm32-wit, or whatever) target triple which uses the canonical ABI. Or perhaps it'd be better exposed as extern "wasm"? While we're essentially stuck with wasm32-unknown-unknown's ABI, I do hope it can be relegated to a deprecated historical accident sometime in the future.

I believe I was right because with some minor tweaks I get past the intial error and see a different one of the same nature. Now that the types line up, the function isn't expected to be provided by the environment anymore.

It looks like the mismatch in type signatures was enabled by going through extern "C".

This is the diff:

diff --git a/src/c2rust/limbs.rs b/src/c2rust/limbs.rs
index e44679e45..a5a472f33 100644
--- a/src/c2rust/limbs.rs
+++ b/src/c2rust/limbs.rs
@@ -11,7 +11,7 @@ extern "C" {
         __function: *const std::os::raw::c_char,
     ) -> !;
 }
-pub type size_t = std::os::raw::c_uint;
+pub type size_t = crate::c::size_t;
 pub type __uint32_t = std::os::raw::c_uint;
 pub type __uint64_t = u64;
 pub type uint32_t = __uint32_t;
diff --git a/src/limb.rs b/src/limb.rs
index 0b8144a57..1e7f6675c 100644
--- a/src/limb.rs
+++ b/src/limb.rs
@@ -19,6 +19,7 @@
 //! limbs use the native endianness.

 use crate::{c, error};
+use core::convert::{TryFrom, TryInto};

 #[cfg(feature = "alloc")]
 use crate::bits;
@@ -44,6 +45,18 @@ pub enum LimbMask {
     False = 0,
 }

+impl TryFrom<Limb> for LimbMask {
+    type Error = error::Unspecified;
+
+    fn try_from(value: Limb) -> Result<Self, Self::Error> {
+        match value {
+            0 => Ok(LimbMask::True),
+            1 => Ok(LimbMask::False),
+            _ => Err(error::Unspecified),
+        }
+    }
+}
+
 #[cfg(target_pointer_width = "32")]
 #[derive(Debug, PartialEq)]
 #[repr(u32)]
@@ -83,7 +96,7 @@ pub fn limbs_less_than_limb_constant_time(a: &[Limb], b: Limb) -> LimbMask {

 #[inline]
 pub fn limbs_are_zero_constant_time(limbs: &[Limb]) -> LimbMask {
-    unsafe { LIMBS_are_zero(limbs.as_ptr(), limbs.len()) }
+    unsafe { LIMBS_are_zero(limbs.as_ptr(), limbs.len()).try_into().expect("Limb should be convertible to LimbMask") }
 }

 #[cfg(feature = "alloc")]
@@ -355,7 +368,7 @@ extern "C" {

     #[cfg(feature = "alloc")]
     fn LIMBS_are_even(a: *const Limb, num_limbs: c::size_t) -> LimbMask;
-    fn LIMBS_are_zero(a: *const Limb, num_limbs: c::size_t) -> LimbMask;
+    fn LIMBS_are_zero(a: *const Limb, num_limbs: c::size_t) -> Limb;
     #[cfg(feature = "alloc")]
     fn LIMBS_equal_limb(a: *const Limb, b: Limb, num_limbs: c::size_t) -> LimbMask;
     fn LIMBS_less_than(a: *const Limb, b: *const Limb, num_limbs: c::size_t) -> LimbMask;

If this is a type mismatch in the upstream, it should be fixed, so I'm sure a PR would be appreciated :smiley:

Agreed. I'm still assessing the extent of all that :slight_smile:

I was wrong. There are definitely several type mismatches, but fixing those doesn't actually solve anything.

All that did was change the order of the imports in the .wasm file so that the error I was getting before wasn't the first error I encountered.

If I use the approach I tried earlier where I remove extern "C" altogether, the function implementation makes it into the binary and import "env" "LIMBS_are_zero" is gone.

I confirmed all of this by using wasm2wat and inspecting the file.

Ah, this may just be because I accidentally reverted some feature flag changes. Hopefully that's it.

I basically reverted all of my changes and now things appear to "just work" without doing anything other than making sure certain things are available when target_arch = "wasm32".

I'm quite confused about why this didn't work before but pleased that it works now. There must have been something that I overlooked.

I can't thank @bjorn3 enough for the following comment because I wasn't able to find anything confirming that this was how things were supposed to work.

I should clarify that the above applies to ring-xous.

I’m still not sure what’s going wrong when trying to using ring but not pursuing that at the moment. Maybe the link attribute is necessary there.