diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -18,3 +18,4 @@ subinclude:cfastmanifest/.hgignore subinclude:linelog/.hgignore +subinclude:examplerust/.hgignore diff --git a/examplerust/.hgignore b/examplerust/.hgignore new file mode 100644 --- /dev/null +++ b/examplerust/.hgignore @@ -0,0 +1,2 @@ +rustexamplerust/Cargo.lock +rustexamplerust/target diff --git a/examplerust/Makefile b/examplerust/Makefile new file mode 100644 --- /dev/null +++ b/examplerust/Makefile @@ -0,0 +1,31 @@ +# Cargo's depfiles use full paths, so we must include the full path +# to the rust library. +DIR := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) + +ifeq ($(shell uname),Darwin) +# On MacOS, we must configure the linker to allow resolution of +# undefined symbols at runtime. +LIBSUFFIX := .dylib +CARGO_ARGS += -C link-args='-Wl,-undefined,dynamic_lookup' +else +LIBSUFFIX := .so +endif + +RUSTLIB := $(DIR)rustexamplerust/target/release/librustexamplerust$(LIBSUFFIX) + +.PHONY: test +test: examplerust/rustexamplerust.so + hg --config extensions.examplerust=examplerust rust + +examplerust/rustexamplerust.so: $(RUSTLIB) + cp $< $@ + +$(RUSTLIB): + cd rustexamplerust && cargo rustc --release -- $(CARGO_ARGS) + +.PHONY: clean +clean: + $(RM) examplerust/rustexamplerust.so + cd rustexamplerust && cargo clean + +-include rustexamplerust/target/release/librustexamplerust.d diff --git a/examplerust/README.md b/examplerust/README.md new file mode 100644 --- /dev/null +++ b/examplerust/README.md @@ -0,0 +1,31 @@ +examplerust +=========== + +This is an example extension written in [Rust](https://www.rust-lang.org/). +The extension adds a single command: `hg rust`, which prints out a short +text string. Its purpose is to show how integration of extensions written +in Rust might occur. + +Building +======== + +The extension uses [rust-cpython](https://github.com/dgrunwald/rust-cpython) +to interface between Python and Rust, in much the same way as the CPython API +is used for extensions written in C. + +To build, run `make` in this directory. This will call `cargo` to compile +the rust library and then copy it into the `examplerust` extension directory. + +Running +======= + +To use the extension, run `hg --config extensions.examplerust=examplerust` +from this directory. + +Implementation Details +====================== + +As with the extensions written in C, there is a Python extension, `examplerust`, +which imports the Rust library, `rustexamplerust`. The Python extension takes +care of registering methods with the Mercurial registry, and then delegates the +execution of the command to the Rust library. diff --git a/examplerust/examplerust/__init__.py b/examplerust/examplerust/__init__.py new file mode 100644 --- /dev/null +++ b/examplerust/examplerust/__init__.py @@ -0,0 +1,21 @@ +# examplerust.py +# +# Copyright 2017 Facebook, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""example extension written in Rust""" + +from . import rustexamplerust + +from mercurial import registrar + +cmdtable = {} +command = registrar.command(cmdtable) + +@command('rust', [ + ('w', 'how', None, 'Exactly how cool is rust?'), +]) +def rust(ui, repo, *args, **kwargs): + """Run something in rust to show how cool it is.""" + return rustexamplerust.rust(ui, repo, *args, **kwargs) diff --git a/examplerust/rustexamplerust/Cargo.toml b/examplerust/rustexamplerust/Cargo.toml new file mode 100644 --- /dev/null +++ b/examplerust/rustexamplerust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rustexamplerust" +version = "0.1.0" +authors = ["Facebook Source Control Team "] + +[lib] +name = "rustexamplerust" +crate-type = ["cdylib"] + +[dependencies.cpython] +version = "0.1" +default-features = false +features = ["python27-sys", "extension-module-2-7"] diff --git a/examplerust/rustexamplerust/src/lib.rs b/examplerust/rustexamplerust/src/lib.rs new file mode 100644 --- /dev/null +++ b/examplerust/rustexamplerust/src/lib.rs @@ -0,0 +1,57 @@ +// rustexamplerust/src/lib.rs: Rust interface for examplerust +// +// Copyright 2017 Facebook, Inc. +// +// This software may be used and distributed according to the terms of the +// GNU General Public License version 2 or any later version. +// + +#[macro_use] +extern crate cpython; +use cpython::exc; +use cpython::{ObjectProtocol, PyBool, PyDict, PyErr, PyObject, PyResult, PyTuple, Python}; + +py_module_initializer!( + rustexamplerust, + initrustexamplerust, + PyInit_rustexamplerust, + |py, m| { + m.add(py, "rust", py_fn!(py, rust(*args, **kwargs)))?; + + Ok(()) + } +); + +/// Example module function. This function uses the passed-in +/// `ui` object to print a status message. +fn rust(py: Python, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + + // Unpack the values of "ui" and "repo" from `args`. Since this + // function accepts arbitrary keyword arguments, we must unpack all + // arguments manually. + if args.len(py) != 2 { + return Err(PyErr::new::( + py, + "Wrong number of arguments", + )); + } + let ui = args.get_item(py, 0); + let _repo = args.get_item(py, 1); + + // Check the value of the "how" item in `kwargs`. Accessing Python + // keyword arguments in Rust is a bit cumbersome. It is preferable + // to unpack arguments like this in the Python wrapper, and pass + // them in as primitive types when possible. This example is + // included here for completeness. + let how = kwargs + .and_then(|x| x.get_item(py, "how")) + .and_then(|x| x.cast_as::(py).ok().map(PyBool::is_true)); + + if how == Some(true) { + ui.call_method(py, "status", ("Rust is very cool!\n",), None)?; + } else { + ui.call_method(py, "status", ("Rust is cool!\n",), None)?; + } + + Ok(py.None()) +}