grub-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[RFC PATCH 2/7] Rust: module build infrastructure


From: Daniel Axtens
Subject: [RFC PATCH 2/7] Rust: module build infrastructure
Date: Tue, 24 Aug 2021 23:32:38 +1000

Make the grub build infrastructure able to detect a Rust toolchain on a
supported platform and build modules partially written in Rust.

In order to use this you will need:

 - to be building for x86_64-emu (more platforms added later)

 - to have a nightly rust toolchain:

    rustup toolchain install nightly

 - to install the Rust standard library sources:

    rustup component add --toolchain nightly rust-src

 - bindgen

    I'm fairly sure I installed this from my distro.

Key components
==============

configure.ac
------------

configure will check for the presence of rustc, cargo and bindgen.

configure will verify that it can build a minimal freestanding static
library for the target.

Teach configure:

 --disable-rust / --enable-rust: force Rust support off/on

 --enable-rust-debug: build rust modules in debug mode rather than release
                      mode

Introduce COND_RUST to mark when rust is available.

gentpl.py
---------

Teach gentpl.py about some rust keywords for building modules. (These
are demonstrated later on in the series.)

grub-core/lib/rust
------------------

This comprises 3 parts: bindings, a core support 'library', and
a target definition.

1. Bindings

Teach the build system how to make Rust bindings of the C funtions with
bindgen, and how to clean up after Rust builds.

2. Core support

Provide a basic Rust interface for grub (lib.rs). Rust code is built in
the freestanding/embedded mode, so we don't have access to the standard
library ('std'). We do provide rust modules with access to 'core' and
'alloc', giving them core features and dynamic memory. To do this we need
to supply 2 helpers:

 - an allocator: teach Rust to call grub_memalign()/grub_free()
   Currently failed Rust allocations will panic; this is definitely something
   I want to fix, as the Linux kernel rust project has done.

 - panic: call grub_fatal when a rust panic occurs.
   In future I want to print a bit more info, but so far I haven't actually
   caused a panic yet.

3. Target

Provide a description of x86_64-emu as a Rust target.json file. This is based on
the autogenerated target.json file, with some changes to represent the compile
flags used in the rest of grub (no mmx or sse, soft-float, mcmodel=large, no 
redzone)
and some general assumptions grub makes (specifically that it runs 
singlethreaded).

This doesn't actually make anything with Rust, but it lays the foundations.

Signed-off-by: Daniel Axtens <dja@axtens.net>
---
 conf/Makefile.common                       |  1 +
 configure.ac                               | 71 ++++++++++++++++++++++
 gentpl.py                                  | 28 ++++++++-
 grub-core/Makefile.am                      | 20 ++++++
 grub-core/lib/rust/bindings.h              |  4 ++
 grub-core/lib/rust/conftest/Cargo.lock     |  7 +++
 grub-core/lib/rust/conftest/Cargo.toml     | 10 +++
 grub-core/lib/rust/conftest/src/lib.rs     | 10 +++
 grub-core/lib/rust/grub/.gitignore         |  1 +
 grub-core/lib/rust/grub/Cargo.toml         |  8 +++
 grub-core/lib/rust/grub/src/lib.rs         | 63 +++++++++++++++++++
 grub-core/lib/rust/targets/x86_64-emu.json | 27 ++++++++
 include/grub/dl.h                          | 21 ++++++-
 13 files changed, 267 insertions(+), 4 deletions(-)
 create mode 100644 grub-core/lib/rust/bindings.h
 create mode 100644 grub-core/lib/rust/conftest/Cargo.lock
 create mode 100644 grub-core/lib/rust/conftest/Cargo.toml
 create mode 100644 grub-core/lib/rust/conftest/src/lib.rs
 create mode 100644 grub-core/lib/rust/grub/.gitignore
 create mode 100644 grub-core/lib/rust/grub/Cargo.toml
 create mode 100644 grub-core/lib/rust/grub/src/lib.rs
 create mode 100644 grub-core/lib/rust/targets/x86_64-emu.json

diff --git a/conf/Makefile.common b/conf/Makefile.common
index 2a1a886f6d52..aa3b24d36f41 100644
--- a/conf/Makefile.common
+++ b/conf/Makefile.common
@@ -125,6 +125,7 @@ TESTS =
 EXTRA_DIST =
 CLEANFILES =
 BUILT_SOURCES =
+RUST_BUILDDIRS =
 
 # Rules for Automake input
 
diff --git a/configure.ac b/configure.ac
index b025e1e84cfb..f75fb3706ad6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -364,6 +364,24 @@ AM_PROG_CC_C_O
 AM_PROG_AS
 AM_PATH_PYTHON([2.6])
 
+rust_target="${target_cpu}-${platform}"
+if ! test -f "$srcdir/grub-core/lib/rust/targets/${rust_target}.json"; then
+  rust_excuse="No rust target JSON file for this platform yet"
+fi
+AC_SUBST(rust_target)
+
+AC_CHECK_PROG(CARGO, [cargo], [yes], [no])
+AC_CHECK_PROG(RUSTC, [rustc], [yes], [no])
+AC_CHECK_PROG(BINDGEN, [bindgen], [yes], [no])
+
+if test x$CARGO = xno ; then
+  rust_excuse="no cargo binary";
+elif test x$RUSTC = xno ; then
+  rust_excuse="no rustc binary";
+elif test x$BINDGEN = xno ; then
+  rust_excuse="no bindgen binary";
+fi
+
 # Must be GCC.
 test "x$GCC" = xyes || AC_MSG_ERROR([GCC is required])
 
@@ -599,6 +617,21 @@ fi
 
 TARGET_CC_VERSION="$(LC_ALL=C $TARGET_CC --version | head -n1)"
 
+if test x"$rust_excuse" = x ; then
+  AC_CACHE_CHECK([whether nightly rustc can compile for $rust_target], 
[grub_cv_target_rustc], [
+  tmp_builddir=`pwd`
+  cd $srcdir/grub-core/lib/rust/conftest;
+  if CARGO_TARGET_DIR=$tmp_builddir/grub-core/lib/rust/conftest/target cargo 
+nightly build --release --target ../targets/${rust_target}.json 
-Zbuild-std=core,alloc 2>/dev/null >/dev/null; then
+    grub_cv_target_rustc=yes;
+  else
+    grub_cv_target_rustc=no;
+    rust_excuse="cannot compile conftest package with rust nightly";
+  fi
+  cd $tmp_builddir
+  rm -rf grub-core/lib/rust/conftest/target
+  ])
+fi
+
 AC_CACHE_CHECK([which extra warnings work], [grub_cv_target_cc_w_extra_flags], 
[
   LDFLAGS="$TARGET_LDFLAGS -nostdlib -static"
 
@@ -1622,6 +1655,32 @@ enable_grub_mkfont=no
 fi
 AC_SUBST([enable_grub_mkfont])
 
+AC_ARG_ENABLE([rust],
+             [AS_HELP_STRING([--enable-rust],
+                             [build Rust language grub modules 
(default=guessed)])])
+
+if test x"$enable_rust" = xyes && test x"$rust_excuse" != x ; then
+  AC_MSG_ERROR([Rust was explicitly requested but can't be compiled 
($rust_excuse)])
+fi
+if test x"$enable_rust" = xno ; then
+  rust_excuse="explictly disabled"
+fi
+
+AC_ARG_ENABLE([rust-debug],
+              AC_HELP_STRING([--enable-rust-debug],
+                             [build Rust code with debugging information 
[default=no]]),
+              [rust_debug_release=$enableval],
+              [rust_debug_release=no])
+
+if test "x$rust_debug_release" = "xyes" ; then
+    RUST_TARGET_SUBDIR=debug
+else
+    RUST_TARGET_SUBDIR=release
+fi
+if test "x$rust_debug_release" = xyes && test "x$rust_excuse" != x ; then
+  AC_MSG_ERROR([Rust debug release specified but cannot enable rust 
($rust_excuse)])
+fi
+
 SAVED_CC="$CC"
 SAVED_CPP="$CPP"
 SAVED_CFLAGS="$CFLAGS"
@@ -1951,6 +2010,10 @@ AC_SUBST(TARGET_LDFLAGS)
 AC_SUBST(TARGET_CPPFLAGS)
 AC_SUBST(TARGET_CCASFLAGS)
 
+RUST_TARGET_PATH="\$(abs_top_srcdir)/grub-core/lib/rust/targets/${rust_target}.json"
+AC_SUBST(RUST_TARGET_PATH)
+AC_SUBST(RUST_TARGET_SUBDIR)
+
 AC_SUBST(TARGET_IMG_LDFLAGS)
 AC_SUBST(TARGET_IMG_CFLAGS)
 AC_SUBST(TARGET_IMG_BASE_LDOPT)
@@ -2031,6 +2094,9 @@ AM_CONDITIONAL([COND_HAVE_ASM_USCORE], [test 
x$HAVE_ASM_USCORE = x1])
 AM_CONDITIONAL([COND_STARFIELD], [test "x$starfield_excuse" = x])
 AM_CONDITIONAL([COND_HAVE_EXEC], [test "x$have_exec" = xy])
 
+AM_CONDITIONAL([COND_RUST], [test x"$rust_excuse" = x])
+AM_CONDITIONAL([RUST_DEBUG_RELEASE], [test x$rust_debug_release = xyes])
+
 test "x$prefix" = xNONE && prefix="$ac_default_prefix"
 test "x$exec_prefix" = xNONE && exec_prefix="${prefix}"
 datarootdir="$(eval echo "$datarootdir")"
@@ -2166,5 +2232,10 @@ echo "With stack smashing protector: Yes"
 else
 echo "With stack smashing protector: No"
 fi
+if [ x"$rust_excuse" = x ]; then
+echo "With rust module support: Yes ($RUST_TARGET_SUBDIR mode)"
+else
+echo "With rust module support: No ($rust_excuse)"
+fi
 echo "*******************************************************"
 ]
diff --git a/gentpl.py b/gentpl.py
index c86550d4f9e5..8820baa73edc 100644
--- a/gentpl.py
+++ b/gentpl.py
@@ -490,6 +490,9 @@ def set_canonical_name_suffix(suffix):
 def cname(defn):
     return canonical_name_re.sub('_', defn['name'] + canonical_name_suffix)
 
+def cratelibname(defn):
+    return canonical_name_re.sub('_', "lib" + defn['name']) + ".a"
+
 def rule(target, source, cmd):
     if cmd[0] == "\n":
         output("\n" + target + ": " + source + cmd.replace("\n", "\n\t") + 
"\n")
@@ -629,6 +632,9 @@ def platform_values(defn, platform, suffix):
 def extra_dist(defn):
     return foreach_value(defn, "extra_dist", lambda value: value + " ")
 
+def rust_sources(defn):
+    return foreach_value(defn, "rust", lambda value: value + " ")
+
 def platform_sources(defn, p): return platform_values(defn, p, "")
 def platform_nodist_sources(defn, p): return platform_values(defn, p, 
"_nodist")
 
@@ -682,14 +688,30 @@ def module(defn, platform):
     gvar_add("MODULE_FILES", name + ".module$(EXEEXT)")
 
     var_set(cname(defn) + "_SOURCES", platform_sources(defn, platform) + " ## 
platform sources")
-    var_set("nodist_" + cname(defn) + "_SOURCES", 
platform_nodist_sources(defn, platform) + " ## platform nodist sources")
-    var_set(cname(defn) + "_LDADD", platform_ldadd(defn, platform))
+    var_set(cname(defn) + "_RUSTSOURCES", rust_sources(defn))
     var_set(cname(defn) + "_CFLAGS", "$(AM_CFLAGS) $(CFLAGS_MODULE) " + 
platform_cflags(defn, platform))
     var_set(cname(defn) + "_LDFLAGS", "$(AM_LDFLAGS) $(LDFLAGS_MODULE) " + 
platform_ldflags(defn, platform))
     var_set(cname(defn) + "_CPPFLAGS", "$(AM_CPPFLAGS) $(CPPFLAGS_MODULE) " + 
platform_cppflags(defn, platform))
     var_set(cname(defn) + "_CCASFLAGS", "$(AM_CCASFLAGS) $(CCASFLAGS_MODULE) " 
+ platform_ccasflags(defn, platform))
     var_set(cname(defn) + "_DEPENDENCIES", "$(TARGET_OBJ2ELF) " + 
platform_dependencies(defn, platform))
 
+    if 'crate' in defn:
+        rustlib = defn['crate'] + 
"/target/$(rust_target)/$(RUST_TARGET_SUBDIR)/" + cratelibname(defn)
+        output("""
+""" + rustlib + ": $(" + cname(defn) + """_RUSTSOURCES) $(COMMON_RUSTSOURCES)
+       cd $(abs_srcdir)/""" + defn['crate'] + """; \\
+       CARGO_TARGET_DIR=$(abs_builddir)/""" + defn['crate'] + """/target cargo 
+nightly build $(CARGO_RELEASE_ARGS) --target=$(RUST_TARGET_PATH) 
-Zbuild-std=core,alloc
+""")
+        gvar_add("RUST_BUILDDIRS", "$(abs_builddir)/" + defn['crate'] + 
"/target/$(rust_target)/$(RUST_TARGET_SUBDIR)/")
+    else:
+        rustlib = ""
+
+    var_set(cname(defn) + "_RUSTLIBRARY", rustlib)
+    var_set(cname(defn) + "_LDADD", platform_ldadd(defn, platform) + " $(" + 
cname(defn) + "_RUSTLIBRARY)")
+    # the rust library needs to be a built source so that automake builds it
+    # before attempting to build the module. putting it in marker and ldadd is 
insufficient
+    var_set("nodist_" + cname(defn) + "_SOURCES", 
platform_nodist_sources(defn, platform) + " $(" + cname(defn) + "_RUSTLIBRARY) 
## platform nodist sources and built rust libraries")
+        
     gvar_add("dist_noinst_DATA", extra_dist(defn))
     gvar_add("BUILT_SOURCES", "$(nodist_" + cname(defn) + "_SOURCES)")
     gvar_add("CLEANFILES", "$(nodist_" + cname(defn) + "_SOURCES)")
@@ -698,7 +720,7 @@ def module(defn, platform):
     gvar_add("MARKER_FILES", name + ".marker")
     gvar_add("CLEANFILES", name + ".marker")
     output("""
-""" + name + """.marker: $(""" + cname(defn) + """_SOURCES) $(nodist_""" + 
cname(defn) + """_SOURCES)
+""" + name + ".marker: $(" + cname(defn) + "_SOURCES) $(nodist_" + cname(defn) 
+ """_SOURCES)
        $(TARGET_CPP) -DGRUB_LST_GENERATOR $(CPPFLAGS_MARKER) $(DEFS) 
$(DEFAULT_INCLUDES) $(INCLUDES) $(""" + cname(defn) + """_CPPFLAGS) $(CPPFLAGS) 
$^ > $@.new || (rm -f $@; exit 1)
        grep 'MARKER' $@.new > $@; rm -f $@.new
 """)
diff --git a/grub-core/Makefile.am b/grub-core/Makefile.am
index ee88e44e97a0..71c1444bcf7d 100644
--- a/grub-core/Makefile.am
+++ b/grub-core/Makefile.am
@@ -26,6 +26,23 @@ CFLAGS_LIBRARY += $(CFLAGS_PLATFORM) -fno-builtin
 CPPFLAGS_LIBRARY += $(CPPFLAGS_PLATFORM)
 CCASFLAGS_LIBRARY += $(CCASFLAGS_PLATFORM)
 
+if RUST_DEBUG_RELEASE
+CARGO_RELEASE_ARGS=
+else
+CARGO_RELEASE_ARGS=--release
+endif
+
+if COND_RUST
+lib/rust/grub/src/bindings.rs: lib/rust/bindings.h
+       bindgen --use-core lib/rust/bindings.h --ctypes-prefix=cty -o 
lib/rust/grub/src/bindings.rs -- -I ../include -I.. -DGRUB_FILE='"<rust 
bindings>"'
+
+CLEANFILES += lib/rust/grub/src/bindings.rs
+
+COMMON_RUSTSOURCES = lib/rust/grub/src/bindings.rs lib/rust/grub/src/lib.rs
+else
+COMMON_RUSTSOURCES =
+endif
+
 build-grub-pep2elf$(BUILD_EXEEXT): $(top_srcdir)/util/grub-pe2elf.c 
$(top_srcdir)/grub-core/kern/emu/misc.c $(top_srcdir)/util/misc.c
        $(BUILD_CC) -o $@ -I$(top_srcdir)/include $(BUILD_CFLAGS) 
$(BUILD_CPPFLAGS) $(BUILD_LDFLAGS) -DGRUB_BUILD=1 -DGRUB_TARGET_WORDSIZE=64 
-DGRUB_UTIL=1 -DGRUB_BUILD_PROGRAM_NAME=\"build-grub-pep2elf\" $^
 CLEANFILES += build-grub-pep2elf$(BUILD_EXEEXT)
@@ -504,3 +521,6 @@ windowsdir: $(PROGRAMS) $(starfield_DATA) $(platform_DATA)
        for x in $(platform_DATA); do \
                cp -fp $$x $(windowsdir)/$(target_cpu)-$(platform)/$$x; \
        done
+
+clean-local:
+       rm -rf $(RUST_BUILDDIRS)
diff --git a/grub-core/lib/rust/bindings.h b/grub-core/lib/rust/bindings.h
new file mode 100644
index 000000000000..3c28eae0d4fe
--- /dev/null
+++ b/grub-core/lib/rust/bindings.h
@@ -0,0 +1,4 @@
+#include <grub/mm.h>
+#include <grub/misc.h>
+#include <grub/normal.h>
+#include <grub/dl.h>
diff --git a/grub-core/lib/rust/conftest/Cargo.lock 
b/grub-core/lib/rust/conftest/Cargo.lock
new file mode 100644
index 000000000000..b666d240a75f
--- /dev/null
+++ b/grub-core/lib/rust/conftest/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "conftest"
+version = "0.1.0"
diff --git a/grub-core/lib/rust/conftest/Cargo.toml 
b/grub-core/lib/rust/conftest/Cargo.toml
new file mode 100644
index 000000000000..065e0ed0d744
--- /dev/null
+++ b/grub-core/lib/rust/conftest/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "conftest"
+version = "0.1.0"
+edition = "2018"
+
+[lib]
+crate_type = ["staticlib"]
+
+
+[dependencies]
diff --git a/grub-core/lib/rust/conftest/src/lib.rs 
b/grub-core/lib/rust/conftest/src/lib.rs
new file mode 100644
index 000000000000..40749f704583
--- /dev/null
+++ b/grub-core/lib/rust/conftest/src/lib.rs
@@ -0,0 +1,10 @@
+#![no_std]
+
+use core::panic::PanicInfo;
+
+pub fn foo() {}
+
+#[panic_handler]
+fn panic(_panic: &PanicInfo<'_>) -> ! {
+    loop {}
+}
diff --git a/grub-core/lib/rust/grub/.gitignore 
b/grub-core/lib/rust/grub/.gitignore
new file mode 100644
index 000000000000..4c8e25833099
--- /dev/null
+++ b/grub-core/lib/rust/grub/.gitignore
@@ -0,0 +1 @@
+src/bindings.rs
diff --git a/grub-core/lib/rust/grub/Cargo.toml 
b/grub-core/lib/rust/grub/Cargo.toml
new file mode 100644
index 000000000000..0e590e8a636e
--- /dev/null
+++ b/grub-core/lib/rust/grub/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "grub"
+version = "0.1.0"
+authors = ["Daniel Axtens <dja@axtens.net>"]
+edition = "2018"
+
+[dependencies]
+cty = "^0.2"
diff --git a/grub-core/lib/rust/grub/src/lib.rs 
b/grub-core/lib/rust/grub/src/lib.rs
new file mode 100644
index 000000000000..c34f332c88af
--- /dev/null
+++ b/grub-core/lib/rust/grub/src/lib.rs
@@ -0,0 +1,63 @@
+#![no_std]
+#![allow(non_camel_case_types)]
+#![allow(non_upper_case_globals)]
+#![allow(non_snake_case)]
+#![allow(unused)]
+#![feature(alloc_error_handler)]
+
+pub mod bindings;
+use bindings::grub_size_t;
+use core::alloc::GlobalAlloc;
+use core::alloc::Layout;
+use core::convert::TryInto;
+use core::panic::PanicInfo;
+use core::ptr;
+
+struct GrubAlloc;
+
+unsafe impl GlobalAlloc for GrubAlloc {
+    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
+        let size: grub_size_t = match layout.size().try_into() {
+            Ok(x) => x,
+            Err(_) => {
+                return ptr::null_mut();
+            }
+        };
+
+        let align: grub_size_t = match layout.align().try_into() {
+            Ok(x) => x,
+            Err(_) => {
+                return ptr::null_mut();
+            }
+        };
+
+        bindings::grub_memalign(align, size) as *mut u8
+    }
+
+    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
+        bindings::grub_free(ptr as _);
+    }
+}
+
+#[global_allocator]
+static grub_alloc: GrubAlloc = GrubAlloc;
+
+/* FIXME: This is annoying, ideally this wouldn't happen or would get
+caught elsewhere. I think we need the failable alloc work */
+#[alloc_error_handler]
+fn on_oom(_layout: Layout) -> ! {
+    // todo, more info
+    unsafe { bindings::grub_fatal("OOM in Rust code\0".as_ptr() as *const _) };
+
+    // grub_fatal should not return but keep compiler happy
+    loop {}
+}
+
+#[panic_handler]
+fn panicker(reason: &PanicInfo) -> ! {
+    // todo, more info
+    unsafe { bindings::grub_fatal("Panic in Rust\0".as_ptr() as *const _) };
+
+    // grub_fatal should not return but keep compiler happy
+    loop {}
+}
diff --git a/grub-core/lib/rust/targets/x86_64-emu.json 
b/grub-core/lib/rust/targets/x86_64-emu.json
new file mode 100644
index 000000000000..00f1a2b6badf
--- /dev/null
+++ b/grub-core/lib/rust/targets/x86_64-emu.json
@@ -0,0 +1,27 @@
+{
+  "arch": "x86_64",
+  "cpu": "x86-64",
+  "env": "gnu",
+  "data-layout": 
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
+  "dynamic-linking": true,
+  "llvm-target": "x86_64-unknown-linux-gnu",
+  "max-atomic-width": 64,
+  "os": "none",
+  "position-independent-executables": true,
+  "pre-link-args": {
+    "gcc": [
+      "-m64"
+    ]
+  },
+  "target-pointer-width": "64",
+  "features": "-mmx,-sse,+soft-float",
+  "relocation-model": "static",
+  "code-model": "large",
+  "disable-redzone": true,
+  "panic-strategy": "abort",
+  "singlethread": true,
+  "no-builtins": true,
+  "stack-probes": {
+    "kind": "none"
+  }
+}
diff --git a/include/grub/dl.h b/include/grub/dl.h
index b3753c9ca262..55e6a48c46e7 100644
--- a/include/grub/dl.h
+++ b/include/grub/dl.h
@@ -38,7 +38,26 @@
 
 #ifndef GRUB_MOD_INIT
 
-#if !defined (GRUB_UTIL) && !defined (GRUB_MACHINE_EMU) && !defined 
(GRUB_KERNEL)
+#if defined(RUST_WRAPPER)
+
+/* Rust modules always use grub_##name##_{init,fini} */
+
+#define GRUB_MOD_INIT(name)    \
+void grub_##name##_init(void); \
+static void __attribute__((used))  \
+grub_mod_init (grub_dl_t mod __attribute__ ((unused))) \
+{ \
+  grub_##name##_init(); \
+}
+
+#define GRUB_MOD_FINI(name)    \
+void grub_##name##_fini(void); \
+static void __attribute__((used)) \
+grub_mod_fini (void) \
+{ \
+  grub_##name##_fini(); \
+}
+#elif !defined (GRUB_UTIL) && !defined (GRUB_MACHINE_EMU) && !defined 
(GRUB_KERNEL)
 
 #define GRUB_MOD_INIT(name)    \
 static void grub_mod_init (grub_dl_t mod __attribute__ ((unused))) 
__attribute__ ((used)); \
-- 
2.30.2




reply via email to

[Prev in Thread] Current Thread [Next in Thread]