diff --git a/Makefile.am b/Makefile.am
index b8aa049da104040ed4464a02029ec004b60e62fd..be4140aa0b0346915d6cbabb3ffe854362c8c34a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -26,7 +26,7 @@ TESTING_TOR_BINARY=$(top_builddir)/src/or/tor$(EXEEXT)
 endif
 
 if USE_RUST
-rust_ldadd=
+rust_ldadd=$(top_builddir)/src/rust/target/release/libtor_util.a
 else
 rust_ldadd=
 endif
@@ -236,3 +236,6 @@ mostlyclean-local:
 	rm -rf $(HTML_COVER_DIR)
 	rm -rf $(top_builddir)/doc/doxygen
 	rm -rf $(TEST_NETWORK_ALL_LOG_DIR)
+
+clean-local:
+	rm -rf $(top_builddir)/src/rust/target
diff --git a/src/common/compat_rust.c b/src/common/compat_rust.c
new file mode 100644
index 0000000000000000000000000000000000000000..366fd4037bd99a1718b81a70e151ad2697c1cb75
--- /dev/null
+++ b/src/common/compat_rust.c
@@ -0,0 +1,39 @@
+/* Copyright (c) 2017, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+/**
+ * \file rust_compat.c
+ * \brief Rust FFI compatibility functions and helpers. This file is only built
+ * if Rust is not used.
+ **/
+
+#include "compat_rust.h"
+#include "util.h"
+
+/**
+ * Free storage pointed to by <b>str</b>, and itself.
+ */
+void
+rust_str_free(rust_str_t str)
+{
+    char *s = (char *)str;
+    tor_free(s);
+}
+
+/**
+ * Return zero-terminated contained string.
+ */
+const char *
+rust_str_get(const rust_str_t str)
+{
+    return (const char *)str;
+}
+
+/* If we were using Rust, we'd say so on startup. */
+rust_str_t
+rust_welcome_string(void)
+{
+    char *s = tor_malloc_zero(1);
+    return (rust_str_t)s;
+}
+
diff --git a/src/common/compat_rust.h b/src/common/compat_rust.h
new file mode 100644
index 0000000000000000000000000000000000000000..752a29b56c911c52d0c391b3b49704389308b2d8
--- /dev/null
+++ b/src/common/compat_rust.h
@@ -0,0 +1,28 @@
+/* Copyright (c) 2017, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+/**
+ * \file rust_compat.h
+ * \brief Headers for rust_compat.c
+ **/
+
+#ifndef TOR_RUST_COMPAT_H
+#define TOR_RUST_COMPAT_H
+
+#include "torint.h"
+
+/**
+ * Strings allocated in Rust must be freed from Rust code again. Let's make
+ * it less likely to accidentally mess up and call tor_free() on it, because
+ * currently it'll just work but might break at any time.
+ */
+typedef uintptr_t rust_str_t;
+
+void rust_str_free(rust_str_t);
+
+const char *rust_str_get(const rust_str_t);
+
+rust_str_t rust_welcome_string(void);
+
+#endif
+
diff --git a/src/common/include.am b/src/common/include.am
index e285ef5f863790f472b47df8d662e228a34ea801..b37b363b82ed363afe66f44d17f08fc74192d51c 100644
--- a/src/common/include.am
+++ b/src/common/include.am
@@ -100,6 +100,11 @@ LIBOR_A_SRC = \
   $(threads_impl_source)				\
   $(readpassphrase_source)
 
+if USE_RUST
+else
+LIBOR_A_SRC += src/common/compat_rust.c
+endif
+
 src/common/src_common_libor_testing_a-log.$(OBJEXT) \
   src/common/log.$(OBJEXT): micro-revision.i
 
@@ -146,6 +151,7 @@ COMMONHEADERS = \
   src/common/compat.h				\
   src/common/compat_libevent.h			\
   src/common/compat_openssl.h			\
+  src/common/compat_rust.h			\
   src/common/compat_threads.h			\
   src/common/compat_time.h			\
   src/common/compress.h			\
diff --git a/src/or/main.c b/src/or/main.c
index 0da43dc23296f490ca4e0dcef783dc8a5e22728d..dcd7ef215e8805329d96be162c6f975f1beb8d4d 100644
--- a/src/or/main.c
+++ b/src/or/main.c
@@ -58,6 +58,7 @@
 #include "circuitlist.h"
 #include "circuituse.h"
 #include "command.h"
+#include "compat_rust.h"
 #include "compress.h"
 #include "config.h"
 #include "confparse.h"
@@ -3039,6 +3040,15 @@ tor_init(int argc, char *argv[])
                  "Expect more bugs than usual.");
   }
 
+  {
+    rust_str_t rust_str = rust_welcome_string();
+    const char *s = rust_str_get(rust_str);
+    if (strlen(s) > 0) {
+      log_notice(LD_GENERAL, "%s", s);
+    }
+    rust_str_free(rust_str);
+  }
+
   if (network_init()<0) {
     log_err(LD_BUG,"Error initializing network; exiting.");
     return -1;
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
new file mode 100644
index 0000000000000000000000000000000000000000..4ac9606ce889d56d273ec04375524aad3c4302eb
--- /dev/null
+++ b/src/rust/Cargo.lock
@@ -0,0 +1,14 @@
+[root]
+name = "tor_util"
+version = "0.0.1"
+dependencies = [
+ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[metadata]
+"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502"
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..527c536323f9ed8b2e02ffaf10875f74ef619172
--- /dev/null
+++ b/src/rust/Cargo.toml
@@ -0,0 +1,14 @@
+[workspace]
+members = ["tor_util"]
+
+[profile.release]
+debug = true
+panic = "abort"
+
+[source.crates-io]
+registry = 'https://github.com/rust-lang/crates.io-index'
+replace-with = 'vendored-sources'
+
+[source.vendored-sources]
+directory = 'vendor'
+
diff --git a/src/rust/include.am b/src/rust/include.am
new file mode 100644
index 0000000000000000000000000000000000000000..e1980490713991c3b2f1f0251bc448d5cb5e77fa
--- /dev/null
+++ b/src/rust/include.am
@@ -0,0 +1,5 @@
+include src/rust/tor_util/include.am
+
+EXTRA_DIST +=\
+	src/rust/Cargo.toml \
+	src/rust/Cargo.lock
diff --git a/src/rust/tor_util/Cargo.toml b/src/rust/tor_util/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..f175fbdfb071e99dbc3bf7c1404ea3f262ccb375
--- /dev/null
+++ b/src/rust/tor_util/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+authors = ["The Tor Project"]
+name = "tor_util"
+version = "0.0.1"
+
+[lib]
+name = "tor_util"
+path = "lib.rs"
+crate_type = ["rlib", "staticlib"]
+
+[dependencies]
+libc = "*"
+
diff --git a/src/rust/tor_util/ffi.rs b/src/rust/tor_util/ffi.rs
new file mode 100644
index 0000000000000000000000000000000000000000..af4bfc41af82db090632d039247612b28c097c29
--- /dev/null
+++ b/src/rust/tor_util/ffi.rs
@@ -0,0 +1,56 @@
+//! FFI functions, only to be called from C.
+//!
+//! Equivalent C versions of these live in `src/common/compat_rust.c`
+
+use std::mem::forget;
+use std::ffi::CString;
+
+use libc;
+use rust_string::RustString;
+
+/// Free the passed `RustString` (`rust_str_t` in C), to be used in place of
+/// `tor_free`().
+///
+/// # Examples
+/// ```c
+/// rust_str_t r_s = rust_welcome_string();
+/// rust_str_free(r_s);
+/// ```
+#[no_mangle]
+#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
+pub unsafe extern "C" fn rust_str_free(_str: RustString) {
+    // Empty body: Just drop _str and we're done (Drop takes care of it).
+}
+
+/// Lends an immutable, NUL-terminated C String.
+///
+/// # Examples
+/// ```c
+/// rust_str_t r_s = rust_welcome_string();
+/// const char *s = rust_str_get(r_s);
+/// printf("%s", s);
+/// rust_str_free(r_s);
+/// ```
+#[no_mangle]
+pub unsafe extern "C" fn rust_str_get(str: RustString) -> *const libc::c_char {
+    let res = str.as_ptr();
+    forget(str);
+    res
+}
+
+/// Returns a short string to announce Rust support during startup.
+///
+/// # Examples
+/// ```c
+/// rust_str_t r_s = rust_welcome_string();
+/// const char *s = rust_str_get(r_s);
+/// printf("%s", s);
+/// rust_str_free(r_s);
+/// ```
+#[no_mangle]
+pub extern "C" fn rust_welcome_string() -> RustString {
+    let s = CString::new("Tor is running with Rust integration. Please report \
+                          any bugs you encouter.")
+            .unwrap();
+    RustString::from(s)
+}
diff --git a/src/rust/tor_util/include.am b/src/rust/tor_util/include.am
new file mode 100644
index 0000000000000000000000000000000000000000..3b877649a3b47c7df4417380a908bf574404e249
--- /dev/null
+++ b/src/rust/tor_util/include.am
@@ -0,0 +1,12 @@
+EXTRA_DIST +=\
+	src/rust/tor_util/Cargo.toml \
+	src/rust/tor_util/lib.rs \
+	src/rust/tor_util/ffi.rs \
+	src/rust/tor_util/rust_string.rs
+
+src/rust/target/release/libtor_util.a: FORCE
+	( cd "$(abs_top_srcdir)/src/rust/tor_util" ; \
+		CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \
+		$(CARGO) build --release --quiet --frozen )
+
+FORCE:
diff --git a/src/rust/tor_util/lib.rs b/src/rust/tor_util/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..79d583d1aedba83813f047f981ba0c565f800e73
--- /dev/null
+++ b/src/rust/tor_util/lib.rs
@@ -0,0 +1,13 @@
+//! C <-> Rust compatibility helpers and types.
+//!
+//! Generically useful, small scale helpers should go here. This goes for both
+//! the C side (in the form of the ffi module) as well as the Rust side
+//! (individual modules per functionality). The corresponding C stuff lives in
+//! `src/common/compat_rust.{c,h}`.
+
+extern crate libc;
+
+mod rust_string;
+pub mod ffi;
+
+pub use rust_string::*;
diff --git a/src/rust/tor_util/rust_string.rs b/src/rust/tor_util/rust_string.rs
new file mode 100644
index 0000000000000000000000000000000000000000..46ec3fd7a8a99e8d25818f0c335d7f746166f344
--- /dev/null
+++ b/src/rust/tor_util/rust_string.rs
@@ -0,0 +1,101 @@
+use std::ffi::CString;
+use std::mem::forget;
+use libc;
+
+/// Compatibility wrapper for strings allocated in Rust and passed to C.
+///
+/// Rust doesn't ensure the safety of freeing memory across an FFI boundary, so
+/// we need to take special care to ensure we're not accidentally calling
+/// `tor_free`() on any string allocated in Rust. To more easily differentiate
+/// between strings that possibly (if Rust support is enabled) were allocated
+/// in Rust, C has the `rust_str_t` helper type. The equivalent on the Rust
+/// side is `RustString`.
+///
+/// Note: This type must not be used for strings allocated in C.
+#[repr(C)]
+#[derive(Debug)]
+pub struct RustString(*mut libc::c_char);
+
+impl RustString {
+    /// Returns a pointer to the underlying NUL-terminated byte array.
+    ///
+    /// Note that this function is not typically useful for Rust callers,
+    /// except in a direct FFI context.
+    ///
+    /// # Examples
+    /// ```
+    /// # use tor_util::RustString;
+    /// use std::ffi::CString;
+    ///
+    /// let r = RustString::from(CString::new("asdf").unwrap());
+    /// let c_str = r.as_ptr();
+    /// assert_eq!(b'a', unsafe { *c_str as u8});
+    /// ```
+    pub fn as_ptr(&self) -> *const libc::c_char {
+        self.0 as *const libc::c_char
+    }
+}
+
+impl From<CString> for RustString {
+    /// Constructs a new `RustString`
+    ///
+    /// # Examples
+    /// ```
+    /// # use tor_util::RustString;
+    /// use std::ffi::CString;
+    ///
+    /// let r = RustString::from(CString::new("asdf").unwrap());
+    /// ```
+    fn from(str: CString) -> RustString {
+        RustString(str.into_raw())
+    }
+}
+
+impl Into<CString> for RustString {
+    /// Reconstructs a `CString` from this `RustString`.
+    ///
+    /// Useful to take ownership back from a `RustString` that was given to C
+    /// code.
+    ///
+    /// # Examples
+    /// ```
+    /// # use tor_util::RustString;
+    /// use std::ffi::CString;
+    ///
+    /// let cs = CString::new("asdf").unwrap();
+    /// let r = RustString::from(cs.clone());
+    /// let cs2 = r.into();
+    /// assert_eq!(cs, cs2);
+    /// ```
+    fn into(self) -> CString {
+        // Calling from_raw is always OK here: We only construct self using
+        // valid CStrings and don't expose anything that could mutate it
+        let ret = unsafe { CString::from_raw(self.0) };
+        forget(self);
+        ret
+    }
+}
+
+impl Drop for RustString {
+    fn drop(&mut self) {
+        // Don't use into() here, because we would need to move out of
+        // self. Same safety consideration. Immediately drop the created
+        // CString, which takes care of freeing the wrapped string.
+        unsafe { CString::from_raw(self.0) };
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::mem;
+    use super::*;
+
+    use libc;
+
+    /// Ensures we're not adding overhead by using RustString.
+    #[test]
+    fn size_of() {
+        assert_eq!(mem::size_of::<*mut libc::c_char>(),
+                   mem::size_of::<RustString>())
+    }
+}
diff --git a/src/rust/tor_util/tests/rust_string.rs b/src/rust/tor_util/tests/rust_string.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1ff605a43cd256125a38e1e8438a4dc77c12eeb3
--- /dev/null
+++ b/src/rust/tor_util/tests/rust_string.rs
@@ -0,0 +1,37 @@
+extern crate tor_util;
+extern crate libc;
+
+use std::ffi::CString;
+use tor_util::RustString;
+
+#[test]
+fn rust_string_conversions_preserve_c_string() {
+    let s = CString::new("asdf foo").unwrap();
+    let r = RustString::from(s.clone());
+    let r2 = RustString::from(s.clone());
+    let c = r2.as_ptr();
+    assert_eq!(unsafe { libc::strlen(c) }, 8);
+    let c_str = r.into();
+    assert_eq!(s, c_str);
+}
+
+#[test]
+fn empty_string() {
+    let s = CString::new("").unwrap();
+    let r = RustString::from(s.clone());
+    let c = r.as_ptr();
+    assert_eq!(unsafe { libc::strlen(c) }, 0);
+    let c_str = r.into();
+    assert_eq!(s, c_str);
+}
+
+#[test]
+fn c_string_with_unicode() {
+    // The euro sign is three bytes
+    let s = CString::new("asd€asd").unwrap();
+    let r = RustString::from(s.clone());
+    let c = r.as_ptr();
+    assert_eq!(unsafe { libc::strlen(c) }, 9);
+    let c_str = r.into();
+    assert_eq!(s, c_str);
+}
diff --git a/src/test/include.am b/src/test/include.am
index 8a465877d3fec0b0848001522be1fa879b590fad..438eaddba8de6b8e597237292a49bb1af5dd8ac3 100644
--- a/src/test/include.am
+++ b/src/test/include.am
@@ -130,6 +130,7 @@ src_test_test_SOURCES = \
 	src/test/test_routerkeys.c \
 	src/test/test_routerlist.c \
 	src/test/test_routerset.c \
+	src/test/test_rust.c \
 	src/test/test_scheduler.c \
 	src/test/test_shared_random.c \
 	src/test/test_socks.c \
diff --git a/src/test/test.c b/src/test/test.c
index 4d2cf1536b087a0bba4633ec18808062532131ce..18805cb3b7afdda27d7e400a091e9acf388de6af 100644
--- a/src/test/test.c
+++ b/src/test/test.c
@@ -1233,6 +1233,7 @@ struct testgroup_t testgroups[] = {
   { "routerkeys/", routerkeys_tests },
   { "routerlist/", routerlist_tests },
   { "routerset/" , routerset_tests },
+  { "rust/", rust_tests },
   { "scheduler/", scheduler_tests },
   { "socks/", socks_tests },
   { "shared-random/", sr_tests },
diff --git a/src/test/test.h b/src/test/test.h
index 3d7d05e77184bb70f4f980b2a370756c9d1e1be7..3dc1c332c7ca4b28e9e8e4f7a6a5004ccdc206a6 100644
--- a/src/test/test.h
+++ b/src/test/test.h
@@ -231,6 +231,7 @@ extern struct testcase_t router_tests[];
 extern struct testcase_t routerkeys_tests[];
 extern struct testcase_t routerlist_tests[];
 extern struct testcase_t routerset_tests[];
+extern struct testcase_t rust_tests[];
 extern struct testcase_t scheduler_tests[];
 extern struct testcase_t storagedir_tests[];
 extern struct testcase_t socks_tests[];
diff --git a/src/test/test_rust.c b/src/test/test_rust.c
new file mode 100644
index 0000000000000000000000000000000000000000..6ad57d6fcb3f3f8bb81970f1f3296fa71c1dd9e9
--- /dev/null
+++ b/src/test/test_rust.c
@@ -0,0 +1,31 @@
+/* Copyright (c) 2017, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#include "orconfig.h"
+#include "compat_rust.h"
+#include "test.h"
+#include "util.h"
+
+static void
+test_welcome_string(void *arg)
+{
+  (void)arg;
+  rust_str_t s = rust_welcome_string();
+  const char *c_str = rust_str_get(s);
+  tt_assert(c_str);
+  size_t len = strlen(c_str);
+#ifdef HAVE_RUST
+  tt_assert(len > 0);
+#else
+  tt_assert(len == 0);
+#endif
+
+ done:
+  rust_str_free(s);
+}
+
+struct testcase_t rust_tests[] = {
+  { "welcome_string", test_welcome_string, 0, NULL, NULL },
+  END_OF_TESTCASES
+};
+