This is a solution for a workshop which gave me some problems. This solution also available via git: https://github.com/skaunov/6991_ws09/. After the code there also couple of theory questions which complimented the workshop, and then also text of workshop exercise duplicated.
A few notes/comments on the solution
I feel like there should be safety arguments where unsafe
is used. The only reason of their absence is that I don't actually understand what is going on in libc
and FFI, and studying it to add the arguments is to not only learn Rustonomicon (which isn't that bad), but also become C programmer (which isn't on the schedule).
Starter code mentioned in the exercise and useful to understand its scope is represented via https://github.com/skaunov/6991_ws09/commit/80fbd6b4fe71557e3bbaf32516b9b87aa01dc178.
The solution is a bit boilerplaty. I would tackle this with macros, but it was overstretch to me to focus both on FFI part and do this improvement. So this code stays a good object if I will need some macros practice.
use libc::{
c_char, c_double, c_int, fclose, fgets, fopen, fscanf, EOF, FILE
};
use std::ffi::CString;
struct File {
stream: *mut FILE,
}
/* At this place _starter_ code contains
`to_c_string(string: &str) -> Vec<i8>` function, which I
replaced in spite of `ffi::CString` methods during debugging,
and didn't want come from this standard solution. */
impl File {
fn open(_path: &str) -> Option<Self> {
let path_c = CString::new(_path)
.expect("it's exercise --- `_path` should be there");
let mode = CString::new("r").unwrap();
let result = unsafe { fopen(path_c.as_ptr(), mode.as_ptr()) };
if result.is_null() {
None
} else {
Some(File { stream: result })
}
}
/// Returns `None` if file couldn't been read (to `String`) OR it
/// doesn't contain EOL.
fn read_string(&mut self) -> Option<String> {
let mut buffer_current = [i8::default(); 512];
/* looks like it's ok to get just long enough start of the line
let it be 512 symbols */
let success = unsafe {
fgets(
buffer_current.as_mut_ptr() as *mut c_char,
// note two bytes for EOL symbols in `buf`
510,
self.stream,
)
};
if success.is_null() {
return None;
}
Some(
buffer_current
.into_iter()
.map(|x| x.to_ne_bytes()[0] as u8)
.skip_while(|x| x == &0)
.map(|x| x as char)
.collect(),
)
}
fn read_i64(&mut self) -> Option<i64> {
let mut result: c_int = Default::default();
let mode = CString::new("%d").unwrap();
match unsafe {
fscanf(self.stream, mode.as_ptr(), &mut result)
} {
EOF | 0 => None,
_ => Some(result.into()),
}
}
fn read_f64(&mut self) -> Option<f64> {
let mut result: c_double = Default::default();
let mode = CString::new("%lf").unwrap();
match unsafe {
fscanf(self.stream, mode.as_ptr(), &mut result)
} {
EOF | 0 => None,
_ => Some(result.into()),
}
}
fn read_char(&mut self) -> Option<char> {
let mut result: c_char = Default::default();
let mode = CString::new(" %c").unwrap();
match unsafe {
fscanf(self.stream, mode.as_ptr(), &mut result)
} {
EOF | 0 => None,
_ => Some(result.to_be_bytes()[0].into()),
}
}
}
impl Drop for File {
fn drop(&mut self) {
println!("Dropping file.");
let success = unsafe { fclose(self.stream) };
if success != 0 {
panic!("...")
}
}
}
fn main() {
let mut file = File::open("data/test_file.txt")
.expect("Could not open file.");
let s = file.read_string().unwrap();
let i = file.read_i64().unwrap();
let f = file.read_f64().unwrap();
let c = file.read_char().unwrap();
println!("{s} {i} {f} {c}");
}
Answers to the theoretical part
Could your type be Copy
, Clone
, Send
or Sync
?
Practically after adding all three of possible traits the output of the toy/exercise app doesn't change (including Miri, which for this exercise on my system just complaining that particular C function shouldn't be run on OS "linux"). Ofc it's way too shallow and not indicative, but at least it doesn't immediately contradict theoretical rationale/reasoning which is put below.
(Technically for Send
or Sync
it's enough to (unsafely) mark the struct
with respective trait
name.)
Copy
My arguments for Copy
and Clone
were same, but experiment shows that there's a formal obstacle to be Copy
.
Never thought about it, but that makes sense.
"the trait Copy
cannot be implemented for this type; the type has a destructor" \
"Copy
not allowed on types with destructors"
Clone
This struct
/type can be Clone
, but must not.
"Can" since pointer at the end of the day is just a number(s), which are even Copy
in their Rust essence. It's easy just to derive
the trait
.
It's really crazy to manage (and de-allocate) this FILE
stream pointer while cloning it around.
Send
Seems like the only sane trait here, since it transfer ownership. I guess it should be noticed people generally should watch if anything is still using the File
before Send
ing it, but since everything is unsafe
here, this is the way to deal with things.
Sync
It seems as an awful idea to me: like Clone
, but on an order of magnitude.
Did you have to think about memory safety at all? Could you modify your code to cause a dangling pointer?
Yes & yes; it's enough to first try to acquire CString
from_raw
and then read its usage limitation documentation.
How easy was inter-operating with C functions?
Near to a nightmare --- it's the first time I touched C since university, and I did avoid to delve it back then already. It's to personal, so better ask a person who was exposed to C previously.
'''
The exercise
Here's copy of relevant text from the workshop (to archive it and for convenience).
(Note that https://github.com/skaunov/6991_ws09/blob/master/data/test_file.txt for read
ing is provided as part of the downloaded starter code.)
Exercise:
Workshop 9 - Safety Shmafety ...
This week, we'll be looking at some unsafe code. Before the workshop, we'll have a quick refresher on the following topics:
- Safe abstractions over unsafe code
- The
unsafe
keyword*const T
/*mut T
...
Week 9 Code
In groups, your task for this week is to build a
File
struct, using primitives from thelibc
crate. These use functions which are common in the C language, but which Rust does not use.You should implement the following on the starter code:
- Opening a file to read with the
read
function.- Reading a string a file with
read
.- Reading a type from a file with
read_i64
.- Reading a type from a file with
read_f64
.- Reading a type from a file with
read_char
.- When your file goes out of scope, close it automatically.
To do this, you will need to know about the following functions/enums/traits, mostly from
libc
:
- The
FILE
enum represents an open file.- fopen opens a file (called
filename
). To open in "read" mode, you should use"r"
as the mode. This returns a (possibly null) file-pointer, which represents an open file.- fgets reads a whole line from a file.
buf
is a pointer to memory, which must be at leastn
bytes big.stream
is a file-pointer. Note that fgets returns null if opening the file failed.- fscanf reads a different type from the file
stream
, depending onformat
: * If the string is"%d"
, the third argument tofscanf
should be a&mut libc::c_int
. * If the string is" %c"
(note the leading space), the third argument tofscanf
should be a&mut libc::c_char
. * If the string is"%lf"
, the third argument tofscanf
should be a&mut libc::c_double
.- fclose should close the file pointer given to it. The Drop trait may be useful here.
- There are a variety of types (
c_int
,c_char
,c_double
) which correspond to types from C. You will need these.You should assume this code will only be run on Unix-like systems, and that all paths will be ASCII.
At the end of the activity, if there's time, look into the
errno
crate. This allows you to give the user more information about why an operation failed.Afterwards, you might like to discuss the following points:
- Could your type be
Copy
,Clone
,Send
orSync
?- Did you have to think about memory safety at all? Could you modify your code to cause a dangling pointer?
- How easy was interoperating with C functions?
PS AFAIU the only thing that shouldn't be publicly shared are solutions to the graded exercises/activities, which I keep access restricted. If I got anything wrong and this code shouldn't be public as well, pls approach me by any mean you like, and I'll remove it without hesitations. Tom received couple of my contacts due to distant interactions (macrokata
, https://discord.com/channels/1075940806004838470/1075940806004838475/1101303841531646092).