Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I have internal state for a custom logger which only takes &self?

Tags:

rust

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?

like image 359
kkpoon Avatar asked Oct 31 '18 06:10

kkpoon


1 Answers

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 Syncability. 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

like image 178
Cerberus Avatar answered Sep 22 '22 04:09

Cerberus