CSC/ECE 517 Spring 2019 - M1902 Refactor bluetooth support for better maintainability
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. The platform that is mentioned here indicates - Linux, Android, Mac, Empty(other users) and one that tests the feature.
Steps for OSS Project
- 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 bluetooth.rs file corresponding to the changes in adapter.rs file
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
Files(to be)modified
- devices/src/bluetooth.rs (file was modified)
- devices/src/empty.rs (file was modified)
- devices/src/adapter/rs (file was added)
Previous Implementation
Initially, a cross-platform implementation was written as all the platforms were defined using enum and macros. The Bluez,Android,ac, Empty and Mock are the various platforms. (Empty accounts for ones whic does not fall into Mac, Linux or Android and Mock is a test variant that tests bluetooth feature). This cross platform implementation is not only very messy and confusing but they also expose APIs to callers. The chart below will give you a little clarity,
#[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>), }
The above snippet of the code just declares 5 variants - Bluez, Android, Mac, Empty and Mock. The variants are matched based on 2 attributes, target_os and feature. "cfg" is a conditional statement in rust. The enum variants have 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.
This was followed by an impl BluetoothAdapter block that implemented all the methods for each of the variants. A snippet of the same is below for your clarity,
impl BluetoothAdapter { #[cfg(all(target_os = "linux", feature = "bluetooth"))] pub fn init() -> Result<BluetoothAdapter, Box<Error>> { let bluez_adapter = try!(BluetoothAdapterBluez::init()); Ok(BluetoothAdapter::Bluez(Arc::new(bluez_adapter))) } //init functions for other platforms pub fn get_id(&self) -> String { get_inner_and_call!(self, BluetoothAdapter, get_id) } #[cfg(feature = "bluetooth-test")] pub fn set_id(&self, id: String) { match self { &BluetoothAdapter::Mock(ref fake_adapter) => fake_adapter.set_id(id), _ => (), } } //35 more functions } }
Current Implementation
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
In the diagram below you will clearly see the difference in the two implementations.
The new implementation is implemented in parts below, A snippet of the code in adapter.rs for platform 'Linux' is given below and it consists of trait BluetoothAdapter, The code snippet attached below consists of trait BluetoothAdapter.
Basically, trait is a collection of methods that are defined for an unknown type. Here, a trait is defined as a group of methods. This trait can be used to implement any data type, like Linux, Mac, Android,etc. The group of methods are the same functions we saw in 'impl BluetoothAdapter' in the previous impementation.
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>>; }
BluetoothAdapter is imported from bluetooth_adapter, and that is imported from the crate blurz. This is named as BluetoothAdapterBluez for convenience.
#[cfg(all(target_os = "linux", feature = "bluetooth"))] use blurz::bluetooth_adapter::BluetoothAdapter as BluetoothAdapterBluez;
Here, if target_os and feature is matched, a structure for Linux platform is created as Bluez.
#[cfg(all(target_os = "linux", feature = "bluetooth"))] struct Bluez(Arc<BluetoothAdapterBluez>);
If target_os and feature is matched, BluetoothAdapter trait is implemented for Bluez which allows the use of methods of BluetoothAdapter that we previously mentioned. All the methods listed under the trait will be used here. In the function 'get_devices(&self)', device_list gets the list of all the devices via get_device_list() method and try! matches the given Result(Here, Result is an enum type used for returning and propagating errors). Ok() is a variant of enum 'Result' and a map method is implemented to create a new session for each of the device.
#[cfg(all(target_os = "linux", feature = "bluetooth"))] impl BluetoothAdapter for Bluez{ pub fn get_id(&self) -> String { get_inner_and_call!(self, BluetoothAdapter, get_id) //functions 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()) } //more functions }
Snippet code above focuses on two types of methods. Most of the methods in the implementation were called using macros in the previous implementation. Macros are used to conditionally call the functions for the respective platform. Our trait implementations directly call the appropriate platform-specific code, rather than using a macro. In the older implementation, 'get_inner_and_call!(self, BluetoothAdapter, get_id)' was used which is now replaces by self.0 as there is a separate BluetoothAdapter implementation for each platform so using self now references to itself which was not possible in the previous cross platform code. The other type of methods are APIs that use adapter as an argument. impl BluetoothAdapter used two such methods - get_devices() where BluetoothDevice was involved and create_discovery_session(&self)where BluetoothDiscoverySession was involved. This method was modified by inlining the required code from impl BluetoothDevice and impl BluetoothDiscoverySession into these methods.
The code below is create_discovery_session(&self) implemented in the new trait,
fn create_discovery_session(&self) -> Result<BluetoothDiscoverySession, Box<Error>> { let bluez_session = BluetoothDiscoverySessionBluez{}; Ok(BluetoothDiscoverySession::Bluez(Arc::new(bluez_session))) }
Similarly, other platform implementations are done using the trait 'Bluetooth Adapter'.
Pull Request
A pull request, which was created, can be viewed here
The modifications made in the devices crate were approved by the servo team. The pull request was successfully merged.
The photo below shows the pull request we made, titled 'Added adapter.rs'.
The merge that took place is shown in the following screenshot,
The additional photo below shows that Travis Cl build was passed successfuly!
Test Plan
The project does not have a direct output(visual or audio). The refactoring only focuses on making the code follow good rust practices that involves changing the type of an entire crate from enum to trait. The only way to check if the new code written is right, by doing the following,
Use the command cargo build on the cloned devices folder to check if the code compiles.
To check if the changes made does not alter the previous implementation, clone servo/servo repository and build it(as per the build instructions given at the end of this page).
Run the tests below to check if they 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
These instructions test each feature of bluetooth to check if it works as required.
Note: The tests were written and suggested by the servo team
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
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
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
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