getditto / safer_ffi

Write safer FFI code in Rust without polluting it with unsafe code

Home Page:http://getditto.github.io/safer_ffi

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Pushing to a safer-ffi Vec does not change `len` or `cap`

TheButlah opened this issue · comments

I have the following binding code:

#[ffi_export]
pub fn my_push(vec: &mut ::safer_ffi::vec::Vec<u8>, item: ::safer_ffi::boxed::Box<u8>) {
    let item: u8 = *item.into();
    vec.with_rust_mut(|vec| vec.push(item))
}

I pass a Vec_u8_t* to the function on the C side (this is actually being done in C# where I have my struct and take a reference to it and cast that to an IntPtr and pass that). It doesn't appear as though the len or cap fields actually change however.

This is very worrysome indeed 💯, but since you mention C#, I suspect it may have to do with how it is marshalled. Soon safer-ffi will make its experimental support of C# headers official, which are designed to minimize the marshalling interference from C#'s runtime.

To confirm my hypothesis, could you share how you've wrapped that Vec<u8>?


Using the experimental (and not yet released) C# support of safer-ffi, for instance, I get:

[StructLayout(LayoutKind.Sequential, Size = 24)]
public unsafe struct Vec_uint8_t {
    public byte * ptr;
    public UIntPtr len;
    public UIntPtr cap;
}

public unsafe partial class Ffi {
    [DllImport(RustLib, ExactSpelling = true)] public static unsafe extern
    void my_push (
        Vec_uint8_t * vec,
        byte * item);
}
  • Aside: you shouldn't need to Box a u8 when using #[ffi_export] 🙂

we are using cppsharp to do the generation. It creates some PInvoke DLLImports, and also some higher level wrappers on top of those. The wrappers are insufficient for our use because they don't seem to capture ownership semantics, so we are using the pinvoke statements that cppsharp generates directly.

The original code example I gave is a simplification, here is the real one:

[SuppressUnmanagedCodeSecurity, DllImport("tp_client", EntryPoint = "tp_client__contract__properties__channels__RVec_KeyframeU32__push", CallingConvention = __CallingConvention.Cdecl)]
internal static extern void TpClientContractPropertiesChannelsRVecKeyframeU32Push(__IntPtr vec, __IntPtr item);
#[::safer_ffi::ffi_export]
pub fn tp_client__contract__properties__channels__RVec_KeyframeU32__push(vec: &mut ::safer_ffi::vec::Vec<Keyframe_U32>, item: ::safer_ffi::boxed::Box<Keyframe_U32>) {
    let item: $t = *item.into();
    vec.with_rust_mut(|vec| vec.push(item))
}

This is a partial expansion of some cursed macros that handle name mangling and monomorphization of our types 😅

Here is our wrapper on top of the DllImport (yes its cursed, if you have suggestions lmk)

using generated = tp_client.generated;
using RSharp;
using IntPtr = System.IntPtr;

// This file is manually implemented for now but will be autogenerated eventually

namespace Teleportal.Client.Contract.Properties.Channels
{
    using struct_RVec_Keyframe_U8 = tp_client.Vec_tp_client_contract_properties_channelsKeyframeU8.__Internal;
    using struct_RVec_Keyframe_U8_ptr = tp_client.Vec_tp_client_contract_properties_channelsKeyframeU8_ptr.__Internal;
    using struct_RVec_Keyframe_U8_const_ptr = tp_client.Vec_tp_client_contract_properties_channelsKeyframeU8_const_ptr.__Internal;

    public sealed class RVec_Keyframe_U8 : RVec<Keyframe_U8, struct_RVec_Keyframe_U8>
    {

        // TODO: Make versions from const_ptr and ptr
        public RVec_Keyframe_U8(struct_RVec_Keyframe_U8 inner) : base(inner) { }

        public RVec_Keyframe_U8() : base(NewHelper(generated.__Internal.TpClientContractPropertiesChannelsRVecKeyframeU8New)) { }

        override protected void NativeDrop(struct_RVec_Keyframe_U8 inner)
        {
            generated.__Internal.TpClientContractPropertiesChannelsRVecKeyframeU8Drop(inner);
        }

        public unsafe override void push(Keyframe_U8 e)
        {
            if (!e.Inner.HasValue)
            {
                throw new System.Exception("Tried to push value that was already disposed!");
            }
            if (!this.Inner.HasValue)
            {
                throw new System.Exception("Tried to call a method on a value that was already disposed!");
            }
            var shared = this.Inner.Value;
            generated.__Internal.TpClientContractPropertiesChannelsRVecKeyframeU8Push((IntPtr)(&shared), e.Inner.Value.p);
            e.Inner = null;  // move from `e`
        }

        public unsafe override Keyframe_U8 this[int index]
        {
            get
            {
                var inner = this.Inner.Value;
                var inner_ptr = &inner;
                var item = new Ptr<Keyframe_U8>(generated.__Internal.TpClientContractPropertiesChannelsRVecKeyframeU8Get((IntPtr)(inner_ptr), (ulong)index));
                return new Keyframe_U8(item, OwnershipSemantics.SharedRef);
            }

            set
            {
                if (this.OwnershipSemantics == OwnershipSemantics.SharedRef)
                {
                    throw new OwnershipException("`this` is a SharedRef");
                }
                if (value.OwnershipSemantics != OwnershipSemantics.Owned)
                {
                    throw new OwnershipException("`value` is not Owned");
                }
                var val_ptr = value.Inner.Value;
                value.Inner = null;  // Move from `value`

                var array = new Ptr<Keyframe_U8>(this.Inner.Value.ptr);
                generated.__Internal.TpClientContractPropertiesChannelsRVecKeyframeU8Set(array.p, (ulong)index, val_ptr.p);
            }
        }
    }
}

var shared = this.Inner.Value;

so this is the copy I was suspecting could be happening: you'll need to this.Inner.Value = shared; after the FFI call to make sure the change is propagated to this.Inner and thus this

Thank you! That was indeed the problem. Sorry for the trouble, closing as resolved