CSC/ECE 517 Spring 2019 - M1902 Refactor bluetooth support for better maintainability

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

Servo

Servo is a prototype web browser engine, developed on 64-bit macOS, 64-bit Linux, 64-bit Windows, and Android. The goal is to create a new layout engine using a modern programming language (Rust), and using parallelism and code safety, to achieve greater security and performance versus contemporary browsers.

For more information, click here

Rust

Rust is a new open-source systems programming language created by Mozilla and a community of volunteers, designed to help developers create fast, secure applications which take full advantage of the powerful features of modern multi-core processors. It prevents segmentation faults and guarantees thread safety, all with an easy-to-learn syntax. Rust is syntactically similar to C++, but is designed to provide better memory safety while maintaining high performance.

For more information, click here

WebBluetooth - Problem Statement

The WebBluetooth specification allows websites to interact with active bluetooth devices in the vicinity of a user. Servo is a new, experimental browser that implements this specification; since bluetooth support requires lots of per-platform code, the current implementation is messy. The goal of this project is to implement clean separation of a cross-platform interface from the specific per-platform implementation to make the code more maintainable.

Implementation

The current implementation uses values of type enum and they are to be converted to trait. If every variant is known, enum is preferred. If not, then trait object is preferred. Traits make it easier to add new implementations to the code than enum.

Trait

A trait is a rust feature that tells the compiler about functionality a type must provide. Traits are the abstract mechanism for adding functionality to types and establishing relationships between them.

Take, for example, the following simple trait for hashing:

trait Hash {
    fn hash(&self) -> u64;
}

In order to implement this trait for a given type, you must provide a hash method with matching signature:

impl Hash for bool {
    fn hash(&self) -> u64 {
        if *self { 0 } else { 1 }
    }
}

impl Hash for i64 {
    fn hash(&self) -> u64 {
        *self as u64
    }
}

Unlike interfaces in languages like Java, C#, new traits can be implemented for existing types (as with Hash above). That means abstractions can be created after-the-fact, and applied to existing libraries.

You can get more information on traits here

Steps

  • To convert the BluetoothAdapter type from an enum to a trait.
  • To create a new adapter.rs file (and therefore module) that contains implementations of this new trait for all platforms.
  • To modify Servo's integration to use this new trait, replacing uses of the old BluetoothAdapter enum with a trait object.
  • To convert the BluetoothDiscoverySession type from an enum to a trait.
  • To create a new discovery_session.rs file that contains implementations of this new trait for all platforms.
  • To modify Servo's integration to use this new trait, replacing uses of the old BluetoothDiscoverySession enum with a trait object.

Files(to be)modified

In servo repository,

  • servo/components/bluetooth/lib.rs

In devices repository,

  • devices/src/bluetooth.rs

Converting enum to Trait

Initially, a cross-platform implementation was written as all the platforms were defined using enum and macros. The Bluez, Android, etc. names below are enum variants to refer to, with the real type stored inside using Arc<T>. Arc<T> is a smart pointer that automatically dereferences to the inner type. If you want to know more about Arc<T> click here.

#[derive(Clone, Debug)]
pub enum BluetoothAdapter {
    #[cfg(all(target_os = "linux", feature = "bluetooth"))]
    Bluez(Arc<BluetoothAdapterBluez>),
    #[cfg(all(target_os = "android", feature = "bluetooth"))]
    Android(Arc<BluetoothAdapterAndroid>),
    #[cfg(all(target_os = "macos", feature = "bluetooth"))]
    Mac(Arc<BluetoothAdapterMac>),
    #[cfg(not(any(all(target_os = "linux", feature = "bluetooth"),
                  all(target_os = "android", feature = "bluetooth"),
                  all(target_os = "macos", feature = "bluetooth"))))]
    Empty(Arc<BluetoothAdapterEmpty>),
    #[cfg(feature = "bluetooth-test")]
    Mock(Arc<FakeBluetoothAdapter>),
}

Trait is used for per-platform implementation.

  • A trait for BluetoothAdapter is defined
  • A separate structure for each platform (say Android, linux, Mac) is created
  • Functions corresponding to each platform is written in the respective structure

Referring to the enum code that exists in the bluetooth.rs file, you will be able to see that macros are used to conditionally call the functions for the repective platform. Our trait implementations to directly call the appropriate platform-specific code, rather than using a macro.

A snippet of the code in adapter.rs for platform 'Linux' is given below,

pub trait BluetoothAdapter{
    fn get_id(&self)-> String;
    fn get_devices(&self)-> Result<Vec<BluetoothDevice>, Box<Error>>;
    fn get_device(&self, address: String) -> Result<Option<BluetoothDevice>, Box<Error>>;
    fn get_address(&self) -> Result<String, Box<Error>>;
    fn get_name(&self)-> Result<String, Box<Error>>;
    fn get_alias(&self) -> Result<String, Box<Error>>;
    fn get_class(&self)-> Result<u32, Box<Error>>;
    fn is_powered(&self)-> Result<bool, Box<Error>>;
    fn is_discoverable(&self) -> Result<bool, Box<Error>>;
    fn is_pairable(&self)-> Result<bool, Box<Error>>;
    fn get_pairable_timeout(&self) -> Result<u32, Box<Error>>;
    fn get_discoverable_timeout(&self)-> Result<u32, Box<Error>>;
    fn is_discovering(&self)-> Result<bool, Box<Error>>;
    fn create_discovery_session(&self) -> Result<BluetoothDiscoverySession, Box<Error>> ;
    fn get_uuids(&self)-> Result<Vec<String>, Box<Error>>;
    fn get_vendor_id_source(&self)-> Result<String, Box<Error>>;
    fn get_vendor_id(&self)-> Result<u32, Box<Error>>;
    fn get_product_id(&self) -> Result<u32, Box<Error>> ;
    fn get_device_id(&self) -> Result<u32, Box<Error>>;
    fn get_modalias(&self) -> Result<(String, u32, u32, u32), Box<Error>>; 
}
#[derive(Clone, Debug)]
#[cfg(all(target_os = "linux", feature = "bluetooth"))]
struct Bluez(Arc<BluetoothAdapterBluez>);

#[cfg(all(target_os = "linux", feature = "bluetooth"))]
impl BluetoothAdapter for Bluez{

    fn get_id(&self) -> String{
        self.0.get_id()
    }

    fn get_devices(&self) -> Result<Vec<BluetoothDevice>, Box<Error>>{
        let device_list = try!(self.0.get_device_list());
        Ok(device_list.into_iter().map(|device|  BluetoothDevice::Bluez(Arc::new(BluetoothDeviceBluez::new(device)))).collect())
    }

    fn get_device(&self, address: String) -> Result<Option<BluetoothDevice>, Box<Error>> {
        let devices = try!(self.get_devices());
        for device in devices {
            if try!(device.get_address()) == address {
                return Ok(Some(device));
            }
        }
        Ok(None)
    }

    fn get_address(&self) -> Result<String, Box<Error>> {
        self.0.get_address()
    }

    fn get_name(&self) -> Result<String, Box<Error>> {
        self.0.get_name()
    }


    fn get_alias(&self) -> Result<String, Box<Error>> {
        self.0.get_alias()
    }


    fn get_class(&self) -> Result<u32, Box<Error>> {
        self.0.get_class()
    }

    fn is_powered(&self) -> Result<bool, Box<Error>> {
        self.0.is_powered()
    }


    fn is_discoverable(&self) -> Result<bool, Box<Error>> {
       self.0.is_discoverable()
    }


    fn is_pairable(&self) -> Result<bool, Box<Error>> {
       self.0.is_pairable()
    }


    fn get_pairable_timeout(&self) -> Result<u32, Box<Error>> {
       self.0.get_pairable_timeout()
    }

    fn get_discoverable_timeout(&self) -> Result<u32, Box<Error>> {
       self.0.get_discoverable_timeout()
    }

    fn is_discovering(&self) -> Result<bool, Box<Error>> {
       self.0.is_discovering()
    }


    fn create_discovery_session(&self) -> Result<BluetoothDiscoverySession, Box<Error>> {
        let bluez_adapter = try!(BluetoothAdapterBluez::init());
        let bluez_session = try!(BluetoothDiscoverySessionBluez::create_session(Arc::new(bluez_adapter)));
        Ok(BluetoothDiscoverySession::Bluez(Arc::new(bluez_session)))
    }

    fn get_uuids(&self) -> Result<Vec<String>, Box<Error>> {
        self.0.get_uuids()
    }


    fn get_vendor_id_source(&self) -> Result<String, Box<Error>> {
        self.0.get_vendor_id_source()
    }

    fn get_vendor_id(&self) -> Result<u32, Box<Error>> {
        self.0.get_vendor_id()
    }

    fn get_product_id(&self) -> Result<u32, Box<Error>> {
        self.0.get_product_id()
    }

    fn get_device_id(&self) -> Result<u32, Box<Error>> {
        self.0.get_device_id()
    }

    fn get_modalias(&self) -> Result<(String, u32, u32, u32), Box<Error>> {
        self.0.get_modalias()
    }
}   

Similarly, other platform implementations are done using the trait 'Bluetooth Adapter'.

A pull request, which has been created, can be viewed here

Conversion of the BluetoothDiscoverySession type from an enum to a trait, which requires a new discovery_session.rs file, will be a continuation for the next project.

Setting up your environment

Servo is built with Cargo, the Rust package manager. We also use Mozilla's Mach tools to orchestrate the build and other tasks.

Installing Rust

Building servo requires rustup, version 1.8.0 or more recent. If you have an older version, run rustup self update.

To install on Windows, download and run rustup-init.exe then follow the onscreen instructions.

To install on other systems, run:

curl https://sh.rustup.rs -sSf | sh

This will also download the current stable version of Rust, which Servo won’t use. To skip that step, run instead:

curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain none

Local build instructions for Debian-based Linuxes are given below:

1. Run ./mach bootstrap. If this fails, run the commands below:

   sudo apt install git curl autoconf libx11-dev \
  libfreetype6-dev libgl1-mesa-dri libglib2.0-dev xorg-dev \
  gperf g++ build-essential cmake virtualenv python-pip \
  libssl1.0-dev libbz2-dev libosmesa6-dev libxmu6 libxmu-dev \
  libglu1-mesa-dev libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev \
  libharfbuzz-dev ccache clang \
  libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev autoconf2.13 

If you using a version prior to Ubuntu 17.04 or Debian Sid, replace libssl1.0-dev with libssl-dev. Additionally, you'll need a local copy of GStreamer with a version later than 12.0. You can place it in support/linux/gstreamer/gstreamer, or run ./mach bootstrap-gstreamer to set it up.

If you are using Ubuntu 16.04 run export HARFBUZZ_SYS_NO_PKG_CONFIG=1 before building to avoid an error with harfbuzz.

If you are on Ubuntu 14.04 and encountered errors on installing these dependencies involving libcheese, see #6158 for a workaround. You may also need to install gcc 4.9, clang 4.0, and cmake 3.2:

If virtualenv does not exist, try python-virtualenv.

Local build instructions for Windows environments are given below:

1. Install Python for Windows (https://www.python.org/downloads/release/python-2714/).

The Windows x86-64 MSI installer is fine. You should change the installation to install the "Add python.exe to Path" feature.

2. Install virtualenv.

In a normal Windows Shell (cmd.exe or "Command Prompt" from the start menu), do:

pip install virtualenv

If this does not work, you may need to reboot for the changed PATH settings (by the python installer) to take effect.

3. Install Git for Windows (https://git-scm.com/download/win). DO allow it to add git.exe to the PATH (default settings for the installer are fine).

4. Install Visual Studio Community 2017 (https://www.visualstudio.com/vs/community/).

You MUST add "Visual C++" to the list of installed components. It is not on by default. Visual Studio 2017 MUST installed to the default location or mach.bat will not find it.

If you encountered errors with the environment above, do the following for a workaround:

Download and install Build Tools for Visual Studio 2017

Install python2.7 x86-x64 and virtualenv


On macOS (homebrew)

1. brew bundle install --file=etc/taskcluster/macos/Brewfile

2. pip install virtualenv

Build instructions for all other environments are available here

Normal build

To build Servo in development mode. This is useful for development, but the resulting binary is very slow.

For Mac OS,

git clone https://github.com/servo/servo

cd servo

./mach build --dev

For Windows,

git clone https://github.com/servo/servo

cd servo

mach.bat build -d to build

If you have troubles with x64 type prompt as mach.bat set by default:

you may need to choose and launch the type manually, such as x86_x64 Cross Tools Command Prompt for VS 2017 in the Windows menu.)

cd to/the/path/servo

python mach build -d

Note that build may take up to 45 minutes.

Detailed instructions for setting up the environment and the dependancies are available here

Running the tests

The project does not have a direct output. If the refactoring is done as needed, the tests below should pass.

For Mac OS,

Use ./mach test-wpt tests/wpt/mozilla/tests/bluetooth/ to run the existing bluetooth automated tests.

For Windows,

Use mach.bat test-wpt tests/wpt/mozilla/tests/bluetooth/ to run the existing bluetooth automated tests

References

https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html

https://doc.rust-lang.org/rust-by-example/

https://github.com/servo/servo/#the-servo-parallel-browser-engine-project

https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#overriding-dependencies

https://github.com/servo/servo/labels/I-refactor