├── src ├── hello │ ├── feed │ │ └── readme.md │ ├── native │ │ ├── .gitignore │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── dotnet │ │ └── src │ │ │ ├── .gitignore │ │ │ ├── Nuget.Config │ │ │ ├── Hello.csproj │ │ │ └── Program.cs │ └── build.ps1 ├── byte_array │ ├── feed │ │ └── readme.md │ ├── native │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── dotnet │ │ ├── src │ │ │ ├── .gitignore │ │ │ ├── Nuget.Config │ │ │ ├── ByteArray.csproj │ │ │ ├── src.sln │ │ │ └── Native.cs │ │ └── test │ │ │ ├── .gitignore │ │ │ ├── Nuget.Config │ │ │ ├── ByteArray.Tests.csproj │ │ │ └── ResizeBufTests.cs │ └── test.ps1 └── pooled │ └── native │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── lib.rs ├── .gitignore ├── README.md ├── DotNetFFI.sln └── .gitattributes /src/hello/feed/readme.md: -------------------------------------------------------------------------------- 1 | # Local feed -------------------------------------------------------------------------------- /src/byte_array/feed/readme.md: -------------------------------------------------------------------------------- 1 | # Local feed -------------------------------------------------------------------------------- /src/hello/native/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/byte_array/native/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/pooled/native/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | *.user 3 | *.dll 4 | *.exe 5 | *.nupkg 6 | .idea -------------------------------------------------------------------------------- /src/hello/dotnet/src/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | bin/ 4 | obj/ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | .vscode 9 | .vs -------------------------------------------------------------------------------- /src/byte_array/dotnet/src/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | bin/ 4 | obj/ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | .vscode 9 | .vs -------------------------------------------------------------------------------- /src/byte_array/dotnet/test/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | bin/ 4 | obj/ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | .vscode 9 | .vs -------------------------------------------------------------------------------- /src/hello/build.ps1: -------------------------------------------------------------------------------- 1 | cargo-nuget pack --cargo-dir .\native\ --nupkg-dir .\feed\ 2 | dotnet restore .\dotnet\src\dotnet.csproj --configfile .\Nuget.Config -------------------------------------------------------------------------------- /src/hello/native/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[no_mangle] 2 | pub extern fn hello_from_rust(a: i32) { 3 | println!("Hello from Rust: {}", a); 4 | } 5 | 6 | #[no_mangle] 7 | pub extern fn say_hello(cb: extern fn(a: i32)) { 8 | cb(7); 9 | } -------------------------------------------------------------------------------- /src/pooled/native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native" 3 | version = "0.1.0" 4 | authors = ["Ashley Mannix "] 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | libc = "*" 11 | slab = "*" 12 | pool = "*" -------------------------------------------------------------------------------- /src/hello/dotnet/src/Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/hello/native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_native" 3 | version = "0.0.1" 4 | description = "A library for testing cargo nuget" 5 | authors = ["Ashley Mannix "] 6 | 7 | [lib] 8 | crate-type = ["dylib"] 9 | 10 | [dependencies] 11 | libc = "*" -------------------------------------------------------------------------------- /src/byte_array/native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bytearray_native" 3 | version = "0.0.1" 4 | description = "A library for testing cargo nuget" 5 | authors = ["Ashley Mannix "] 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | libc = "*" -------------------------------------------------------------------------------- /src/byte_array/test.ps1: -------------------------------------------------------------------------------- 1 | cargo-nuget pack --cargo-dir .\native\ --nupkg-dir .\feed\ 2 | dotnet restore .\dotnet\src\ByteArray.csproj --configfile .\Nuget.Config 3 | dotnet restore .\dotnet\test\ByteArray.Tests.csproj --configfile .\Nuget.Config 4 | 5 | dotnet test .\dotnet\test\ByteArray.Tests.csproj -------------------------------------------------------------------------------- /src/hello/dotnet/src/Hello.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp1.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/src/Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/test/Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/src/ByteArray.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | True 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/test/ByteArray.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/hello/dotnet/src/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | class Native 5 | { 6 | // A simple external Rust function 7 | [DllImport("hello_native", ExactSpelling = true)] 8 | static extern void hello_from_rust(int a); 9 | 10 | // Call `hello_from_rust` 11 | public void HelloFromRust(int a) 12 | { 13 | hello_from_rust(a); 14 | } 15 | 16 | // An external Rust function that'll execute a fn pointer 17 | [DllImport("hello_native", ExactSpelling = true)] 18 | static extern void say_hello(say_hello_cb cb); 19 | 20 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 21 | delegate void say_hello_cb(int a); 22 | 23 | // Call `say_hello` 24 | public void SayHello() 25 | { 26 | say_hello(a => 27 | { 28 | Console.WriteLine($"Hello from C#: {a}"); 29 | }); 30 | } 31 | } 32 | 33 | class Program 34 | { 35 | static void Main(string[] args) 36 | { 37 | var native = new Native(); 38 | 39 | native.HelloFromRust(5); 40 | native.SayHello(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/src/src.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.0.0 5 | MinimumVisualStudioVersion = 10.0.0.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteArray", "ByteArray.csproj", "{758CD596-A781-4D33-A38E-1B7F67893E42}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ByteArray.Tests", "..\test\ByteArray.Tests.csproj", "{4D8EB88E-570A-4768-9104-08DE42288266}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {758CD596-A781-4D33-A38E-1B7F67893E42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {758CD596-A781-4D33-A38E-1B7F67893E42}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {758CD596-A781-4D33-A38E-1B7F67893E42}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {758CD596-A781-4D33-A38E-1B7F67893E42}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {4D8EB88E-570A-4768-9104-08DE42288266}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {4D8EB88E-570A-4768-9104-08DE42288266}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {4D8EB88E-570A-4768-9104-08DE42288266}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {4D8EB88E-570A-4768-9104-08DE42288266}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/byte_array/native/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::mem; 4 | use libc::size_t; 5 | 6 | #[repr(C)] 7 | pub struct ResizeBuf { 8 | ptr: *mut u8, 9 | len: size_t, 10 | cap: size_t, 11 | } 12 | 13 | impl ResizeBuf { 14 | /// Build a `ResizeBuf` structure from a native `Vec`. 15 | /// 16 | /// This will leak memory unless `to_vec` is called. 17 | fn from_vec(mut buf: Vec) -> Self { 18 | let ptr = buf.as_mut_ptr(); 19 | let len = buf.len(); 20 | let cap = buf.capacity(); 21 | 22 | mem::forget(buf); 23 | 24 | ResizeBuf { 25 | ptr: ptr, 26 | len: len, 27 | cap: cap, 28 | } 29 | } 30 | 31 | /// Convert this `ResizeBuf` into a native `Vec`. 32 | /// 33 | /// This will drop the value when it goes out of scope unless `from_vec` is called. 34 | unsafe fn to_vec(self) -> Vec { 35 | Vec::::from_raw_parts(self.ptr, self.len, self.cap) 36 | } 37 | } 38 | 39 | #[no_mangle] 40 | pub extern fn alloc(size: size_t) -> ResizeBuf { 41 | println!("allocating buffer: {}", size); 42 | 43 | let buf = Vec::::with_capacity(size as usize); 44 | 45 | ResizeBuf::from_vec(buf) 46 | } 47 | 48 | #[no_mangle] 49 | pub extern fn reserve(buf: ResizeBuf, reserve: size_t) -> ResizeBuf { 50 | println!("reserving {} extra bytes", reserve); 51 | 52 | let mut buf = unsafe { buf.to_vec() }; 53 | 54 | buf.reserve(reserve); 55 | 56 | ResizeBuf::from_vec(buf) 57 | } 58 | 59 | #[no_mangle] 60 | pub extern fn drop(buf: ResizeBuf) { 61 | let buf = unsafe { buf.to_vec() }; 62 | 63 | println!("contains: {:?}", buf); 64 | println!("dropping buffer"); 65 | } 66 | -------------------------------------------------------------------------------- /src/pooled/native/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate slab; 2 | extern crate pool; 3 | 4 | use slab::Slab; 5 | use pool::{Pool, Dirty, Checkout}; 6 | 7 | // TODO: Make this thread safe 8 | struct MemPool { 9 | checkouts: Slab>>, 10 | buffer: Pool> 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use slab::Slab; 16 | use pool::{Pool, Dirty}; 17 | use super::MemPool; 18 | 19 | #[test] 20 | fn it_works() { 21 | // Create a memory pool 22 | let mut pool = MemPool { 23 | checkouts: Slab::with_capacity(1), 24 | buffer: Pool::with_capacity(1, 4, || Dirty(())) 25 | }; 26 | 27 | // Get an entry from the pool, store a handle to it and return its index and pointer 28 | let (idx, ptr) = { 29 | let buf = pool.buffer.checkout().unwrap(); 30 | let mut entry = pool.checkouts 31 | .vacant_entry() 32 | .unwrap() 33 | .insert(buf); 34 | 35 | let ptr = entry 36 | .get_mut() 37 | .extra_mut() 38 | .as_mut_ptr(); 39 | 40 | (entry.index(), ptr) 41 | }; 42 | 43 | // Someone writing to this memory from C# 44 | { 45 | let mut slice = unsafe { ptr.as_mut().unwrap() }; 46 | *slice = 1; 47 | } 48 | 49 | // The pool is now full so we can't checkout anymore 50 | assert!(pool.buffer.checkout().is_none()); 51 | 52 | // Remove the entry and let it fall out of scope 53 | { 54 | let _ = pool.checkouts.remove(idx).unwrap(); 55 | } 56 | 57 | // The pool is not full so we can checkout again 58 | let entry = pool.buffer.checkout().unwrap(); 59 | 60 | let bytes = entry.extra(); 61 | assert_eq!(&[1,0,0,0,0,0,0,0], bytes); 62 | } 63 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This sample is a bit out of date now. For something more current and mucu more fleshed out, [see here](https://github.com/KodrAus/rust-csharp-ffi). 2 | 3 | # Rust/CSharp Interop 4 | 5 | C# is capable of interopping with native C code. It's a common feature that projects like Kestrel use this heavily to communicate with `libuv` for efficient asynchronous io. 6 | 7 | I'm not so keen on the idea of writing C though, I'd much rather write Rust. This repo is a playground for interop between Rust and C#, which can be done in the same way. 8 | 9 | These samples use [`cargo-nuget`](https://github.com/KodrAus/cargo-nuget) for building and packaging the Rust libraries. 10 | 11 | ## What would be nice 12 | 13 | - Share byte arrays between Rust and C# 14 | - Poll a C# `Task` from Rust as a `Future` 15 | - `await` a Rust `Future` in C# 16 | 17 | ## What to do 18 | 19 | ### Work out conventions between the Rust and C# ends 20 | 21 | Who owns the memory? If it's allocated in Rust then it's freed in Rust, but who initiates that call? In Rust it's possible to invalidate a structure by moving it into a new shape (from `Vec` to `Buf`). So you simply require exclusive ownership of that instance. In C# we can't really do that, and once a buf has been sliced around it's not safe to dispose in Rust so long as those slices are still active. 22 | 23 | So maybe we can look at the same ref-counting pattern used by `OwnedBuffer` in `corefxlab` and require that drops to 0 before returning ownership of the buffer to Rust. Or alternatively we can use Rust ref counting for all things and once that count drops to 0 from borrows in Rust or C# the buffer is dropped. That means a p/invoke call for each new reference though. 24 | -------------------------------------------------------------------------------- /DotNetFFI.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ByteArray", "src\byte_array\dotnet\src\ByteArray.csproj", "{2E79E146-5615-4F00-A84E-220524EF5812}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hello", "src\hello\dotnet\src\Hello.csproj", "{E6BCEA8C-806A-4E1D-9944-9A7D81B56027}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ByteArray.Tests", "src\byte_array\dotnet\test\ByteArray.Tests.csproj", "{DAEA0065-A27F-445C-B7C8-B276B54242B6}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {2E79E146-5615-4F00-A84E-220524EF5812}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {2E79E146-5615-4F00-A84E-220524EF5812}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {2E79E146-5615-4F00-A84E-220524EF5812}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {2E79E146-5615-4F00-A84E-220524EF5812}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {E6BCEA8C-806A-4E1D-9944-9A7D81B56027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {E6BCEA8C-806A-4E1D-9944-9A7D81B56027}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {E6BCEA8C-806A-4E1D-9944-9A7D81B56027}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {E6BCEA8C-806A-4E1D-9944-9A7D81B56027}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DAEA0065-A27F-445C-B7C8-B276B54242B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DAEA0065-A27F-445C-B7C8-B276B54242B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DAEA0065-A27F-445C-B7C8-B276B54242B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DAEA0065-A27F-445C-B7C8-B276B54242B6}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /src/byte_array/dotnet/test/ResizeBufTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | // ReSharper disable HeapView.ObjectAllocation.Evident 5 | // ReSharper disable HeapView.ClosureAllocation 6 | 7 | namespace ByteArray 8 | { 9 | public class ResizeBufTests 10 | { 11 | [Fact] 12 | public void Convert_To_Reader_Multiple_Times() 13 | { 14 | using (var writer = new ResizeBufWriter()) 15 | { 16 | writer.ToReader(); 17 | 18 | Assert.Throws(() => writer.ToReader()); 19 | } 20 | } 21 | 22 | [Fact] 23 | public void Multi_Write_And_Read() 24 | { 25 | var data = new Span(new byte[] {0, 1, 2, 3, 4, 5, 6}); 26 | 27 | var writer = new ResizeBufWriter(); 28 | 29 | writer.Write(data.Slice(0, 3)); 30 | writer.Write(data.Slice(3, 4)); 31 | 32 | var reader = writer.ToReader(); 33 | 34 | Assert.Equal(data.ToArray(), reader.Slice().ToArray()); 35 | } 36 | 37 | [Fact] 38 | public void Read_Empty() 39 | { 40 | var reader = new ResizeBufReader(); 41 | 42 | Assert.Equal(Span.Empty.ToArray(), reader.Slice().ToArray()); 43 | } 44 | 45 | [Fact] 46 | public void Single_Write_And_Read() 47 | { 48 | var data = new byte[] {0, 1, 2, 3}; 49 | 50 | var writer = new ResizeBufWriter(); 51 | writer.Write(data); 52 | 53 | var reader = writer.ToReader(); 54 | 55 | Assert.Equal(data, reader.Slice().ToArray()); 56 | } 57 | 58 | [Fact] 59 | public void Write_After_Conversion_To_Reader() 60 | { 61 | using (var writer = new ResizeBufWriter()) 62 | { 63 | writer.ToReader(); 64 | 65 | Assert.Throws(() => writer.Write(Span.Empty)); 66 | } 67 | } 68 | 69 | [Fact] 70 | public void Write_After_Dispose() 71 | { 72 | var writer = new ResizeBufWriter(); 73 | writer.Dispose(); 74 | 75 | Assert.Throws(() => writer.Write(Span.Empty)); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /src/byte_array/dotnet/src/Native.cs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | A simple reader/writer for a Rust Vec that can be resized. 4 | 5 | */ 6 | 7 | using System; 8 | using System.Runtime.InteropServices; 9 | 10 | namespace ByteArray 11 | { 12 | internal static class Native 13 | { 14 | [DllImport("bytearray_native", EntryPoint = "alloc", ExactSpelling = true)] 15 | internal static extern ResizeBuf Alloc(UIntPtr size); 16 | 17 | [DllImport("bytearray_native", EntryPoint = "drop", ExactSpelling = true)] 18 | internal static extern void Drop(ResizeBuf buf); 19 | 20 | [DllImport("bytearray_native", EntryPoint = "reserve", ExactSpelling = true)] 21 | internal static extern ResizeBuf Reserve(ResizeBuf buf, UIntPtr reserve); 22 | } 23 | 24 | [StructLayout(LayoutKind.Sequential)] 25 | internal struct ResizeBuf 26 | { 27 | public IntPtr ptr; 28 | public UIntPtr len; 29 | public IntPtr cap; 30 | } 31 | 32 | // NOTE: This class is not thread-safe and doesn't prevent 33 | // reads during writes. Use `ResizeBufReader` and `ResizeBufWriter` 34 | // instead. 35 | // 36 | // If I rethink the ownership strategy though it'll probably do something 37 | // here. 38 | internal class ResizeBufHandle : SafeHandle 39 | { 40 | private bool _allocated; 41 | private ResizeBuf _value; 42 | 43 | public ResizeBufHandle() 44 | : base(IntPtr.Zero, true) 45 | { 46 | } 47 | 48 | public override bool IsInvalid => false; 49 | 50 | // Get a span for the written part of the buf 51 | public Span Read() 52 | { 53 | if (IsClosed) 54 | throw new ObjectDisposedException("ResizeBuf already closed"); 55 | 56 | return ValueSpan().Slice(0, (int) _value.len); 57 | } 58 | 59 | // Append some bytes to the end of the buf 60 | // TODO: Support writing into part of the vec 61 | public void Write(Span data) 62 | { 63 | if (IsClosed) 64 | throw new ObjectDisposedException("ResizeBuf already closed"); 65 | 66 | Write(data, false); 67 | } 68 | 69 | // A method to copy the contents of a buffer to the our native buffer. 70 | // This isn't optimised, so it's simple and readable. 71 | private void Write(Span data, bool resized) 72 | { 73 | while (true) 74 | { 75 | var dataLen = data.Length; 76 | 77 | if (!_allocated) 78 | { 79 | // If the buffer isn't allocated then go and do that 80 | 81 | _value = Native.Alloc(new UIntPtr((uint) dataLen)); 82 | _allocated = true; 83 | 84 | resized = true; 85 | continue; 86 | } 87 | 88 | // If the buffer is allocated, make sure it has enough space 89 | 90 | var available = (int) _value.cap - (int) _value.len; 91 | 92 | if (dataLen > available) 93 | { 94 | // If there isn't enough space, resize and try write again 95 | 96 | if (resized) 97 | throw new Exception("Already tried to resize ResizeBuf"); 98 | 99 | var needed = dataLen - available; 100 | 101 | _value = Native.Reserve(_value, new UIntPtr((uint) needed)); 102 | 103 | resized = true; 104 | continue; 105 | } 106 | 107 | // If the buffer is allocated and there's space, then copy the bits 108 | 109 | Span slice; 110 | 111 | slice = ValueSpan().Slice((int) _value.len); 112 | 113 | data.CopyTo(slice); 114 | _value.len += dataLen; 115 | break; 116 | } 117 | } 118 | 119 | // Get a span for the entire buf 120 | // This may span uninitialised memory 121 | private unsafe Span ValueSpan() 122 | { 123 | return new Span(_value.ptr.ToPointer(), (int) _value.cap); 124 | } 125 | 126 | protected override bool ReleaseHandle() 127 | { 128 | Native.Drop(_value); 129 | return true; 130 | } 131 | } 132 | 133 | // A writer for a Rust Vec. 134 | public class ResizeBufWriter : IDisposable 135 | { 136 | // ReSharper disable once HeapView.ObjectAllocation.Evident 137 | private readonly object _writeLock = new object(); 138 | 139 | private ResizeBufHandle _handle; 140 | 141 | private ResizeBufWriter(ResizeBufHandle handle) 142 | { 143 | _handle = handle; 144 | } 145 | 146 | public ResizeBufWriter() : this(new ResizeBufHandle()) 147 | { 148 | } 149 | 150 | public void Dispose() 151 | { 152 | lock (_writeLock) 153 | { 154 | if (_handle == null) return; 155 | _handle.Dispose(); 156 | _handle = null; 157 | } 158 | } 159 | 160 | public void Write(Span data) 161 | { 162 | lock (_writeLock) 163 | { 164 | if (_handle == null) 165 | throw new InvalidOperationException("ResizeBuf is not writable"); 166 | 167 | _handle.Write(data); 168 | } 169 | } 170 | 171 | public ResizeBufReader ToReader() 172 | { 173 | lock (_writeLock) 174 | { 175 | if (_handle == null) 176 | throw new InvalidOperationException("ResizeBuf is not writable"); 177 | 178 | var reader = new ResizeBufReader(_handle); 179 | _handle = null; 180 | 181 | return reader; 182 | } 183 | } 184 | } 185 | 186 | public class ResizeBufReader 187 | { 188 | private readonly ResizeBufHandle _handle; 189 | 190 | internal ResizeBufReader(ResizeBufHandle handle) 191 | { 192 | _handle = handle; 193 | } 194 | 195 | public ResizeBufReader() : this(new ResizeBufHandle()) 196 | { 197 | } 198 | 199 | public Span Slice() 200 | { 201 | return _handle.Read(); 202 | } 203 | } 204 | } --------------------------------------------------------------------------------