Skip to content

Commit badb451

Browse files
bors[bot]morrowa
andcommitted
Merge #901
901: Implement sendfile on FreeBSD and Darwin r=Susurrus a=morrowa This PR exposes the `sendfile` system call on libc's supported BSD-likes: * FreeBSD * Darwin (macOS/iOS) DragonFly could be supported in the future, but I was unable to build rustc to test. Note that NetBSD has no equivalent system call. Co-authored-by: Andrew Morrow <andrew.d.morrow@gmail.com>
2 parents b87d45b + 325c43c commit badb451

File tree

5 files changed

+305
-10
lines changed

5 files changed

+305
-10
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66
## [Unreleased]
77

88
### Added
9+
- Added `sendfile` on FreeBSD and Darwin.
10+
([#901](https://github.com/nix-rust/nix/pull/901))
911
- Added `pselect`
1012
([#894](https://github.com/nix-rust/nix/pull/894))
1113
- Exposed `preadv` and `pwritev` on the BSDs.

src/sys/mod.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ pub mod reboot;
4949

5050
pub mod select;
5151

52-
// TODO: Add support for dragonfly, freebsd, and ios/macos.
53-
#[cfg(any(target_os = "android", target_os = "linux"))]
52+
#[cfg(any(target_os = "android",
53+
target_os = "freebsd",
54+
target_os = "ios",
55+
target_os = "linux",
56+
target_os = "macos"))]
5457
pub mod sendfile;
5558

5659
pub mod signal;

src/sys/sendfile.rs

+189-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,195 @@ use libc::{self, off_t};
66
use Result;
77
use errno::Errno;
88

9-
pub fn sendfile(out_fd: RawFd, in_fd: RawFd, offset: Option<&mut off_t>, count: usize) -> Result<usize> {
10-
let offset = offset.map(|offset| offset as *mut _).unwrap_or(ptr::null_mut());
9+
/// Copy up to `count` bytes to `out_fd` from `in_fd` starting at `offset`.
10+
///
11+
/// Returns a `Result` with the number of bytes written.
12+
///
13+
/// If `offset` is `None`, `sendfile` will begin reading at the current offset of `in_fd`and will
14+
/// update the offset of `in_fd`. If `offset` is `Some`, `sendfile` will begin at the specified
15+
/// offset and will not update the offset of `in_fd`. Instead, it will mutate `offset` to point to
16+
/// the byte after the last byte copied.
17+
///
18+
/// `in_fd` must support `mmap`-like operations and therefore cannot be a socket.
19+
///
20+
/// For more information, see [the sendfile(2) man page.](http://man7.org/linux/man-pages/man2/sendfile.2.html)
21+
#[cfg(any(target_os = "android", target_os = "linux"))]
22+
pub fn sendfile(
23+
out_fd: RawFd,
24+
in_fd: RawFd,
25+
offset: Option<&mut off_t>,
26+
count: usize,
27+
) -> Result<usize> {
28+
let offset = offset
29+
.map(|offset| offset as *mut _)
30+
.unwrap_or(ptr::null_mut());
1131
let ret = unsafe { libc::sendfile(out_fd, in_fd, offset, count) };
1232
Errno::result(ret).map(|r| r as usize)
1333
}
34+
35+
cfg_if! {
36+
if #[cfg(any(target_os = "freebsd",
37+
target_os = "ios",
38+
target_os = "macos"))] {
39+
use sys::uio::IoVec;
40+
41+
#[allow(missing_debug_implementations)]
42+
struct SendfileHeaderTrailer<'a>(
43+
libc::sf_hdtr,
44+
Option<Vec<IoVec<&'a [u8]>>>,
45+
Option<Vec<IoVec<&'a [u8]>>>,
46+
);
47+
48+
impl<'a> SendfileHeaderTrailer<'a> {
49+
fn new(
50+
headers: Option<&'a [&'a [u8]]>,
51+
trailers: Option<&'a [&'a [u8]]>
52+
) -> SendfileHeaderTrailer<'a> {
53+
let header_iovecs: Option<Vec<IoVec<&[u8]>>> =
54+
headers.map(|s| s.iter().map(|b| IoVec::from_slice(b)).collect());
55+
let trailer_iovecs: Option<Vec<IoVec<&[u8]>>> =
56+
trailers.map(|s| s.iter().map(|b| IoVec::from_slice(b)).collect());
57+
SendfileHeaderTrailer(
58+
libc::sf_hdtr {
59+
headers: {
60+
header_iovecs
61+
.as_ref()
62+
.map_or(ptr::null(), |v| v.as_ptr()) as *mut libc::iovec
63+
},
64+
hdr_cnt: header_iovecs.as_ref().map(|v| v.len()).unwrap_or(0) as i32,
65+
trailers: {
66+
trailer_iovecs
67+
.as_ref()
68+
.map_or(ptr::null(), |v| v.as_ptr()) as *mut libc::iovec
69+
},
70+
trl_cnt: trailer_iovecs.as_ref().map(|v| v.len()).unwrap_or(0) as i32
71+
},
72+
header_iovecs,
73+
trailer_iovecs,
74+
)
75+
}
76+
}
77+
}
78+
}
79+
80+
cfg_if! {
81+
if #[cfg(target_os = "freebsd")] {
82+
use libc::c_int;
83+
84+
libc_bitflags!{
85+
/// Configuration options for [`sendfile`.](fn.sendfile.html)
86+
pub struct SfFlags: c_int {
87+
/// Causes `sendfile` to return EBUSY instead of blocking when attempting to read a
88+
/// busy page.
89+
SF_NODISKIO;
90+
/// Causes `sendfile` to sleep until the network stack releases its reference to the
91+
/// VM pages read. When `sendfile` returns, the data is not guaranteed to have been
92+
/// sent, but it is safe to modify the file.
93+
SF_SYNC;
94+
/// Causes `sendfile` to cache exactly the number of pages specified in the
95+
/// `readahead` parameter, disabling caching heuristics.
96+
SF_USER_READAHEAD;
97+
/// Causes `sendfile` not to cache the data read.
98+
SF_NOCACHE;
99+
}
100+
}
101+
102+
/// Read up to `count` bytes from `in_fd` starting at `offset` and write to `out_sock`.
103+
///
104+
/// Returns a `Result` and a count of bytes written. Bytes written may be non-zero even if
105+
/// an error occurs.
106+
///
107+
/// `in_fd` must describe a regular file or shared memory object. `out_sock` must describe a
108+
/// stream socket.
109+
///
110+
/// If `offset` falls past the end of the file, the function returns success and zero bytes
111+
/// written.
112+
///
113+
/// If `count` is `None` or 0, bytes will be read from `in_fd` until reaching the end of
114+
/// file (EOF).
115+
///
116+
/// `headers` and `trailers` specify optional slices of byte slices to be sent before and
117+
/// after the data read from `in_fd`, respectively. The length of headers and trailers sent
118+
/// is included in the returned count of bytes written. The values of `offset` and `count`
119+
/// do not apply to headers or trailers.
120+
///
121+
/// `readahead` specifies the minimum number of pages to cache in memory ahead of the page
122+
/// currently being sent.
123+
///
124+
/// For more information, see
125+
/// [the sendfile(2) man page.](https://www.freebsd.org/cgi/man.cgi?query=sendfile&sektion=2)
126+
pub fn sendfile(
127+
in_fd: RawFd,
128+
out_sock: RawFd,
129+
offset: off_t,
130+
count: Option<usize>,
131+
headers: Option<&[&[u8]]>,
132+
trailers: Option<&[&[u8]]>,
133+
flags: SfFlags,
134+
readahead: u16
135+
) -> (Result<()>, off_t) {
136+
// Readahead goes in upper 16 bits
137+
// Flags goes in lower 16 bits
138+
// see `man 2 sendfile`
139+
let flags: u32 = ((readahead as u32) << 16) | (flags.bits() as u32);
140+
let mut bytes_sent: off_t = 0;
141+
let hdtr = headers.or(trailers).map(|_| SendfileHeaderTrailer::new(headers, trailers));
142+
let hdtr_ptr = hdtr.as_ref().map_or(ptr::null(), |s| &s.0 as *const libc::sf_hdtr);
143+
let return_code = unsafe {
144+
libc::sendfile(in_fd,
145+
out_sock,
146+
offset,
147+
count.unwrap_or(0),
148+
hdtr_ptr as *mut libc::sf_hdtr,
149+
&mut bytes_sent as *mut off_t,
150+
flags as c_int)
151+
};
152+
(Errno::result(return_code).and(Ok(())), bytes_sent)
153+
}
154+
} else if #[cfg(any(target_os = "ios", target_os = "macos"))] {
155+
/// Read bytes from `in_fd` starting at `offset` and write up to `count` bytes to
156+
/// `out_sock`.
157+
///
158+
/// Returns a `Result` and a count of bytes written. Bytes written may be non-zero even if
159+
/// an error occurs.
160+
///
161+
/// `in_fd` must describe a regular file. `out_sock` must describe a stream socket.
162+
///
163+
/// If `offset` falls past the end of the file, the function returns success and zero bytes
164+
/// written.
165+
///
166+
/// If `count` is `None` or 0, bytes will be read from `in_fd` until reaching the end of
167+
/// file (EOF).
168+
///
169+
/// `hdtr` specifies an optional list of headers and trailers to be sent before and after
170+
/// the data read from `in_fd`, respectively. The length of headers and trailers sent is
171+
/// included in the returned count of bytes written. If any headers are specified and
172+
/// `count` is non-zero, the length of the headers will be counted in the limit of total
173+
/// bytes sent. Trailers do not count toward the limit of bytes sent and will always be sent
174+
/// regardless. The value of `offset` does not affect headers or trailers.
175+
///
176+
/// For more information, see
177+
/// [the sendfile(2) man page.](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/sendfile.2.html)
178+
pub fn sendfile(
179+
in_fd: RawFd,
180+
out_sock: RawFd,
181+
offset: off_t,
182+
count: Option<off_t>,
183+
headers: Option<&[&[u8]]>,
184+
trailers: Option<&[&[u8]]>
185+
) -> (Result<()>, off_t) {
186+
let mut len = count.unwrap_or(0);
187+
let hdtr = headers.or(trailers).map(|_| SendfileHeaderTrailer::new(headers, trailers));
188+
let hdtr_ptr = hdtr.as_ref().map_or(ptr::null(), |s| &s.0 as *const libc::sf_hdtr);
189+
let return_code = unsafe {
190+
libc::sendfile(in_fd,
191+
out_sock,
192+
offset,
193+
&mut len as *mut off_t,
194+
hdtr_ptr as *mut libc::sf_hdtr,
195+
0)
196+
};
197+
(Errno::result(return_code).and(Ok(())), len)
198+
}
199+
}
200+
}

test/test.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ mod test_net;
2222
mod test_nix_path;
2323
mod test_poll;
2424
mod test_pty;
25-
#[cfg(any(target_os = "linux", target_os = "android"))]
25+
#[cfg(any(target_os = "android",
26+
target_os = "freebsd",
27+
target_os = "ios",
28+
target_os = "linux",
29+
target_os = "macos"))]
2630
mod test_sendfile;
2731
mod test_stat;
2832
mod test_unistd;

test/test_sendfile.rs

+104-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
use std::io::prelude::*;
22
use std::os::unix::prelude::*;
33

4-
use tempfile::tempfile;
5-
64
use libc::off_t;
5+
use nix::sys::sendfile::*;
6+
use tempfile::tempfile;
77

8-
use nix::unistd::{close, pipe, read};
9-
use nix::sys::sendfile::sendfile;
8+
cfg_if! {
9+
if #[cfg(any(target_os = "android", target_os = "linux"))] {
10+
use nix::unistd::{close, pipe, read};
11+
} else if #[cfg(any(target_os = "freebsd", target_os = "ios", target_os = "macos"))] {
12+
use std::net::Shutdown;
13+
use std::os::unix::net::UnixStream;
14+
}
15+
}
1016

17+
#[cfg(any(target_os = "android", target_os = "linux"))]
1118
#[test]
12-
fn test_sendfile() {
19+
fn test_sendfile_linux() {
1320
const CONTENTS: &[u8] = b"abcdef123456";
1421
let mut tmp = tempfile().unwrap();
1522
tmp.write_all(CONTENTS).unwrap();
@@ -28,3 +35,95 @@ fn test_sendfile() {
2835
close(rd).unwrap();
2936
close(wr).unwrap();
3037
}
38+
39+
#[cfg(target_os = "freebsd")]
40+
#[test]
41+
fn test_sendfile_freebsd() {
42+
// Declare the content
43+
let header_strings = vec!["HTTP/1.1 200 OK\n", "Content-Type: text/plain\n", "\n"];
44+
let body = "Xabcdef123456";
45+
let body_offset = 1;
46+
let trailer_strings = vec!["\n", "Served by Make Believe\n"];
47+
48+
// Write the body to a file
49+
let mut tmp = tempfile().unwrap();
50+
tmp.write_all(body.as_bytes()).unwrap();
51+
52+
// Prepare headers and trailers for sendfile
53+
let headers: Vec<&[u8]> = header_strings.iter().map(|s| s.as_bytes()).collect();
54+
let trailers: Vec<&[u8]> = trailer_strings.iter().map(|s| s.as_bytes()).collect();
55+
56+
// Prepare socket pair
57+
let (mut rd, wr) = UnixStream::pair().unwrap();
58+
59+
// Call the test method
60+
let (res, bytes_written) = sendfile(
61+
tmp.as_raw_fd(),
62+
wr.as_raw_fd(),
63+
body_offset as off_t,
64+
None,
65+
Some(headers.as_slice()),
66+
Some(trailers.as_slice()),
67+
SfFlags::empty(),
68+
0,
69+
);
70+
assert!(res.is_ok());
71+
wr.shutdown(Shutdown::Both).unwrap();
72+
73+
// Prepare the expected result
74+
let expected_string =
75+
header_strings.concat() + &body[body_offset..] + &trailer_strings.concat();
76+
77+
// Verify the message that was sent
78+
assert_eq!(bytes_written as usize, expected_string.as_bytes().len());
79+
80+
let mut read_string = String::new();
81+
let bytes_read = rd.read_to_string(&mut read_string).unwrap();
82+
assert_eq!(bytes_written as usize, bytes_read);
83+
assert_eq!(expected_string, read_string);
84+
}
85+
86+
#[cfg(any(target_os = "ios", target_os = "macos"))]
87+
#[test]
88+
fn test_sendfile_darwin() {
89+
// Declare the content
90+
let header_strings = vec!["HTTP/1.1 200 OK\n", "Content-Type: text/plain\n", "\n"];
91+
let body = "Xabcdef123456";
92+
let body_offset = 1;
93+
let trailer_strings = vec!["\n", "Served by Make Believe\n"];
94+
95+
// Write the body to a file
96+
let mut tmp = tempfile().unwrap();
97+
tmp.write_all(body.as_bytes()).unwrap();
98+
99+
// Prepare headers and trailers for sendfile
100+
let headers: Vec<&[u8]> = header_strings.iter().map(|s| s.as_bytes()).collect();
101+
let trailers: Vec<&[u8]> = trailer_strings.iter().map(|s| s.as_bytes()).collect();
102+
103+
// Prepare socket pair
104+
let (mut rd, wr) = UnixStream::pair().unwrap();
105+
106+
// Call the test method
107+
let (res, bytes_written) = sendfile(
108+
tmp.as_raw_fd(),
109+
wr.as_raw_fd(),
110+
body_offset as off_t,
111+
None,
112+
Some(headers.as_slice()),
113+
Some(trailers.as_slice()),
114+
);
115+
assert!(res.is_ok());
116+
wr.shutdown(Shutdown::Both).unwrap();
117+
118+
// Prepare the expected result
119+
let expected_string =
120+
header_strings.concat() + &body[body_offset..] + &trailer_strings.concat();
121+
122+
// Verify the message that was sent
123+
assert_eq!(bytes_written as usize, expected_string.as_bytes().len());
124+
125+
let mut read_string = String::new();
126+
let bytes_read = rd.read_to_string(&mut read_string).unwrap();
127+
assert_eq!(bytes_written as usize, bytes_read);
128+
assert_eq!(expected_string, read_string);
129+
}

0 commit comments

Comments
 (0)