I'm trying to implement a simple logger by implementing the log crate.
The logger should behave like this:
[1] First log message
[2] Second log message
[3] Third log message
To implement this, I have my logger struct
struct SeqLogger {
seq: i64,
}
and implement the Log
trait's
fn enabled(&self, metadata: &Metadata) -> bool
fn log(&self, record: &Record)
fn flush(&self)
In log(&self, record: &Record)
implementation, I would do
fn log(&self, record: &Record) {
println!("[{}] {}", self.seq, record.args());
self.seq = self.seq + 1;
}
However, the compiler complains that self
is not mutable. Am I working in a right way to implement this? How can I update the state of the logger without &mut self
?
It seems that the logger
crate doesn't intend for loggers to have any internal state, so it forces them to be shared as immutable. This easies things a lot, in fact, since a logger should usually be shared between threads and used simultaneously, and that's not possible with & mut self
.
However, there's a usual workaround: interior mutability. There's a type std::cell::Cell
designed exactly for that use case: to have a immutable reference to something that should be mutable. Your internal state is simply an integer, so it's Copy
, and we can just try to use Cell
as-is:
extern crate log; // 0.4.5
use log::*;
use std::cell::Cell;
struct SeqLogger {
seq: Cell<i64>,
}
impl Log for SeqLogger {
fn log(&self, record: &Record) {
println!("[{}] {}", self.seq.get(), record.args());
self.seq.set(self.seq.get() + 1);
}
fn enabled(&self, metadata: &Metadata) -> bool { if false {true} else {unimplemented!()} }
fn flush(&self) { unimplemented!(); }
}
However, the compiler immediately becomes angry again:
error[E0277]: `std::cell::Cell<i64>` cannot be shared between threads safely
--> src/lib.rs:9:6
|
9 | impl Log for SeqLogger {
| ^^^ `std::cell::Cell<i64>` cannot be shared between threads safely
|
= help: within `SeqLogger`, the trait `std::marker::Sync` is not implemented for `std::cell::Cell<i64>`
= note: required because it appears within the type `SeqLogger`
This makes sence, since, as I said before, the logger itself must be Sync
, so we must guarantee that it's safe to share its contents too. At the same time, Cell
is not Sync
- exactly because of the interior mutability we're using here. Again, there's a usual way to fix it - Mutex
:
extern crate log; // 0.4.5
use log::*;
use std::cell::Cell;
use std::sync::Mutex;
struct SeqLogger {
seq: Mutex<Cell<i64>>,
}
impl Log for SeqLogger {
fn log(&self, record: &Record) {
let seq = self.seq.lock().unwrap(); // perhaps replace this with match in production
println!("[{}] {}", seq.get(), record.args());
seq.set(seq.get() + 1);
}
fn enabled(&self, metadata: &Metadata) -> bool { if false {true} else {unimplemented!()} }
fn flush(&self) { unimplemented!(); }
}
Now it compiles just fine.
Playground with the last variant
EDIT: According to comments, we can strip one layer of indirection, since Mutex
grants us both the internal mutability (sort of) and the Sync
ability. So we can remove the Cell
and dreference the MutexGuard
directly:
// --snip--
fn log(&self, record: &Record) {
let mut seq = self.seq.lock().unwrap(); // perhaps replace this with match in production
println!("[{}] {}", *seq, record.args());
*seq = *seq + 1;
}
// --snip--
And furthermore, since our state is just an integer, we can use a standard atomic type instead of Mutex
. Note that AtomicI64
is unstable, so you might want to use AtomicIsize
or AtomicUsize
instead:
use std::sync::atomic::{AtomicIsize, Ordering};
struct SeqLogger {
seq: AtomicIsize,
}
impl Log for SeqLogger {
fn log(&self, record: &Record) {
let id = self.seq.fetch_add(1, Ordering::SeqCst);
println!("[{}] {}", id, record.args());
}
// --snip--
}
Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With