Move VM callback to vmclient

Instead of having clients directly register a callback with VS,
implement a Rust level callback interface in vmclient. This saves an
extra binder call on each notification, a bunch of boilerplate code,
and allows us to provide a slightly better interface (e.g. we can use
the Rust DeathReason enum, as elsewhere in vmclient, for instantly
better logging).

I also replaced all our usages of <some_interface>::binder::{...} with
direct access to binder::{...}. That makes it clearer what depends on
the interface itself and what is just generic binder code. I realise
this should be a separate change, but I only realised that after doing
bits of both.

Test: composd_cmd test-compile, observe logs (on both success & failure)
Test: atest -b (to make sure all our tests build)
Test: Presubmits
Change-Id: Iceda8d7b8f8008f9d7a2c51106c2794f09bb378e
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index 0e8952e..9a60bf7 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -36,12 +36,12 @@
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{
     BnVirtFdService, FsStat::FsStat, IVirtFdService, MAX_REQUESTING_DATA,
 };
-use authfs_aidl_interface::binder::{
-    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
-};
 use authfs_fsverity_metadata::{
     get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
 };
+use binder::{
+    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
+};
 
 /// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
 const FORBIDDEN_MODES: Mode = Mode::from_bits_truncate(!0o777);
diff --git a/authfs/service/src/authfs.rs b/authfs/service/src/authfs.rs
index 547d15d..3e8e0e0 100644
--- a/authfs/service/src/authfs.rs
+++ b/authfs/service/src/authfs.rs
@@ -31,9 +31,7 @@
     OutputDirFdAnnotation::OutputDirFdAnnotation, OutputFdAnnotation::OutputFdAnnotation,
 };
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFs::{BnAuthFs, IAuthFs};
-use authfs_aidl_interface::binder::{
-    self, BinderFeatures, Interface, ParcelFileDescriptor, Status, Strong,
-};
+use binder::{self, BinderFeatures, Interface, ParcelFileDescriptor, Status, Strong};
 
 const AUTHFS_BIN: &str = "/system/bin/authfs";
 const AUTHFS_SETUP_POLL_INTERVAL_MS: Duration = Duration::from_millis(50);
diff --git a/authfs/service/src/main.rs b/authfs/service/src/main.rs
index fe8af61..77cac9a 100644
--- a/authfs/service/src/main.rs
+++ b/authfs/service/src/main.rs
@@ -33,7 +33,7 @@
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
     BnAuthFsService, IAuthFsService,
 };
-use authfs_aidl_interface::binder::{
+use binder::{
     self, add_service, BinderFeatures, ExceptionCode, Interface, ProcessState, Status, Strong,
 };
 
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index d9f8964..df52a0e 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -8,8 +8,7 @@
 
 use crate::common::{divide_roundup, CHUNK_SIZE};
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
-use authfs_aidl_interface::binder::{Status, Strong};
-use binder::StatusCode;
+use binder::{Status, StatusCode, Strong};
 use binder_common::rpc_client::connect_rpc_binder;
 use std::convert::TryFrom;
 use std::io;
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 0773652..23a1eb9 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -11,6 +11,7 @@
         "android.system.virtualizationservice-rust",
         "compos_aidl_interface-rust",
         "libanyhow",
+        "libbinder_rs",
         "liblazy_static",
         "liblog_rust",
         "libnested_virt",
diff --git a/compos/common/binder.rs b/compos/common/binder.rs
index 59726c0..d3550f7 100644
--- a/compos/common/binder.rs
+++ b/compos/common/binder.rs
@@ -16,7 +16,7 @@
 
 //! Helper for converting Error types to what Binder expects
 
-use android_system_virtualizationservice::binder::{Result as BinderResult, Status};
+use binder::{Result as BinderResult, Status};
 use log::warn;
 use std::fmt::Debug;
 
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index cd1ece4..770f489 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -19,16 +19,12 @@
 use crate::timeouts::TIMEOUTS;
 use crate::{COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT, DEFAULT_VM_CONFIG_PATH};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
-    DeathReason::DeathReason,
-    IVirtualMachineCallback::{BnVirtualMachineCallback, IVirtualMachineCallback},
     IVirtualizationService::IVirtualizationService,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
 };
-use android_system_virtualizationservice::binder::{
-    BinderFeatures, Interface, ParcelFileDescriptor, Result as BinderResult, Strong,
-};
 use anyhow::{bail, Context, Result};
+use binder::{ParcelFileDescriptor, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
 use log::{info, warn};
 use rustutils::system_properties;
@@ -37,7 +33,7 @@
 use std::num::NonZeroU32;
 use std::path::{Path, PathBuf};
 use std::thread;
-use vmclient::{VmInstance, VmWaitError};
+use vmclient::{DeathReason, VmInstance, VmWaitError};
 
 /// This owns an instance of the CompOS VM.
 pub struct ComposClient(VmInstance);
@@ -119,13 +115,10 @@
             taskProfiles: parameters.task_profiles.clone(),
         });
 
-        let instance = VmInstance::create(service, &config, console_fd, log_fd)
+        let callback = Box::new(Callback {});
+        let instance = VmInstance::create(service, &config, console_fd, log_fd, Some(callback))
             .context("Failed to create VM")?;
 
-        let callback =
-            BnVirtualMachineCallback::new_binder(VmCallback(), BinderFeatures::default());
-        instance.vm.registerCallback(&callback)?;
-
         instance.start()?;
 
         let ready = instance.wait_until_ready(TIMEOUTS.vm_max_time_to_ready);
@@ -163,7 +156,7 @@
     fn wait_for_shutdown(self) {
         let death_reason = self.0.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit);
         match death_reason {
-            Some(vmclient::DeathReason::Shutdown) => info!("VM has exited normally"),
+            Some(DeathReason::Shutdown) => info!("VM has exited normally"),
             Some(reason) => warn!("VM died with reason {:?}", reason),
             None => warn!("VM failed to exit, dropping"),
         }
@@ -228,53 +221,36 @@
     bail!("No VM support available")
 }
 
-struct VmCallback();
-
-impl Interface for VmCallback {}
-
-impl IVirtualMachineCallback for VmCallback {
-    fn onDied(&self, cid: i32, reason: DeathReason) -> BinderResult<()> {
-        log::warn!("VM died, cid = {}, reason = {:?}", cid, reason);
-        Ok(())
-    }
-
-    fn onPayloadStarted(
-        &self,
-        cid: i32,
-        stream: Option<&ParcelFileDescriptor>,
-    ) -> BinderResult<()> {
-        if let Some(pfd) = stream {
-            if let Err(e) = start_logging(pfd) {
+struct Callback {}
+impl vmclient::VmCallback for Callback {
+    fn on_payload_started(&self, cid: i32, stream: Option<&File>) {
+        if let Some(file) = stream {
+            if let Err(e) = start_logging(file) {
                 warn!("Can't log vm output: {}", e);
             };
         }
         log::info!("VM payload started, cid = {}", cid);
-        Ok(())
     }
 
-    fn onPayloadReady(&self, cid: i32) -> BinderResult<()> {
+    fn on_payload_ready(&self, cid: i32) {
         log::info!("VM payload ready, cid = {}", cid);
-        Ok(())
     }
 
-    fn onPayloadFinished(&self, cid: i32, exit_code: i32) -> BinderResult<()> {
+    fn on_payload_finished(&self, cid: i32, exit_code: i32) {
         log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
-        Ok(())
     }
 
-    fn onError(&self, cid: i32, error_code: i32, message: &str) -> BinderResult<()> {
-        log::warn!("VM error, cid = {}, error code = {}, message = {}", cid, error_code, message,);
-        Ok(())
+    fn on_error(&self, cid: i32, error_code: i32, message: &str) {
+        log::warn!("VM error, cid = {}, error code = {}, message = {}", cid, error_code, message);
     }
 
-    fn onRamdump(&self, _cid: i32, _ramdump: &ParcelFileDescriptor) -> BinderResult<()> {
-        // TODO(b/238295267) send this to tombstone?
-        Ok(())
+    fn on_died(&self, cid: i32, death_reason: DeathReason) {
+        log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
     }
 }
 
-fn start_logging(pfd: &ParcelFileDescriptor) -> Result<()> {
-    let reader = BufReader::new(pfd.as_ref().try_clone().context("Cloning fd failed")?);
+fn start_logging(file: &File) -> Result<()> {
+    let reader = BufReader::new(file.try_clone().context("Cloning file failed")?);
     thread::spawn(move || {
         for line in reader.lines() {
             match line {
diff --git a/compos/composd/src/composd_main.rs b/compos/composd/src/composd_main.rs
index ebcd689..5315828 100644
--- a/compos/composd/src/composd_main.rs
+++ b/compos/composd/src/composd_main.rs
@@ -25,8 +25,8 @@
 mod service;
 
 use crate::instance_manager::InstanceManager;
-use android_system_composd::binder::{register_lazy_service, ProcessState};
 use anyhow::{Context, Result};
+use binder::{register_lazy_service, ProcessState};
 use log::{error, info};
 use std::panic;
 use std::sync::Arc;
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index 9a0935c..75671d7 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -20,7 +20,7 @@
 use crate::instance_starter::{CompOsInstance, InstanceStarter};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use anyhow::{bail, Result};
-use compos_aidl_interface::binder::Strong;
+use binder::Strong;
 use compos_common::compos_client::VmParameters;
 use compos_common::{
     CURRENT_INSTANCE_DIR, DEX2OAT_CPU_SET_PROP_NAME, DEX2OAT_THREADS_PROP_NAME,
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 111c719..2923ee0 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -21,9 +21,9 @@
     IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
 };
 use anyhow::{Context, Result};
+use binder::{ParcelFileDescriptor, Strong};
 use binder_common::lazy_service::LazyServiceGuard;
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
-use compos_aidl_interface::binder::{ParcelFileDescriptor, Strong};
 use compos_common::compos_client::{ComposClient, VmParameters};
 use compos_common::{COMPOS_DATA_ROOT, IDSIG_FILE, IDSIG_MANIFEST_APK_FILE, INSTANCE_IMAGE_FILE};
 use log::info;
diff --git a/compos/composd/src/odrefresh_task.rs b/compos/composd/src/odrefresh_task.rs
index 8fd574c..100fc50 100644
--- a/compos/composd/src/odrefresh_task.rs
+++ b/compos/composd/src/odrefresh_task.rs
@@ -22,8 +22,8 @@
     ICompilationTask::ICompilationTask,
     ICompilationTaskCallback::{FailureReason::FailureReason, ICompilationTaskCallback},
 };
-use android_system_composd::binder::{Interface, Result as BinderResult, Strong};
 use anyhow::{Context, Result};
+use binder::{Interface, Result as BinderResult, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::{
     CompilationMode::CompilationMode, ICompOsService,
 };
diff --git a/compos/composd/src/service.rs b/compos/composd/src/service.rs
index a9b8202..49cfd3a 100644
--- a/compos/composd/src/service.rs
+++ b/compos/composd/src/service.rs
@@ -26,10 +26,8 @@
         ApexSource::ApexSource, BnIsolatedCompilationService, IIsolatedCompilationService,
     },
 };
-use android_system_composd::binder::{
-    self, BinderFeatures, ExceptionCode, Interface, Status, Strong, ThreadState,
-};
 use anyhow::{Context, Result};
+use binder::{self, BinderFeatures, ExceptionCode, Interface, Status, Strong, ThreadState};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::CompilationMode::CompilationMode;
 use compos_common::binder::to_binder_result;
 use compos_common::odrefresh::{PENDING_ARTIFACTS_SUBDIR, TEST_ARTIFACTS_SUBDIR};
diff --git a/compos/src/compilation.rs b/compos/src/compilation.rs
index e14cd94..ab228e1 100644
--- a/compos/src/compilation.rs
+++ b/compos/src/compilation.rs
@@ -32,7 +32,7 @@
     },
     IAuthFsService::IAuthFsService,
 };
-use authfs_aidl_interface::binder::Strong;
+use binder::Strong;
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::CompilationMode::CompilationMode;
 use compos_common::odrefresh::ExitCode;
 
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 088d41a..baf444e 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -30,12 +30,10 @@
 use crate::artifact_signer::ArtifactSigner;
 use crate::compilation::{odrefresh, OdrefreshContext};
 use crate::compos_key;
+use binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::{
     BnCompOsService, CompilationMode::CompilationMode, ICompOsService,
 };
-use compos_aidl_interface::binder::{
-    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong,
-};
 use compos_common::binder::to_binder_result;
 use compos_common::odrefresh::{is_system_property_interesting, ODREFRESH_PATH};
 
@@ -128,7 +126,7 @@
             system_server_compiler_filter,
         ))?;
 
-        let authfs_service = authfs_aidl_interface::binder::get_interface(AUTHFS_SERVICE_NAME)?;
+        let authfs_service = binder::get_interface(AUTHFS_SERVICE_NAME)?;
         let exit_code = to_binder_result(
             odrefresh(&self.odrefresh_path, context, authfs_service, |output_dir| {
                 // authfs only shows us the files we created, so it's ok to just sign everything
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 224cde7..2ece8f5 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -19,7 +19,7 @@
 
 use android_logger::LogId;
 use anyhow::{bail, Context, Result};
-use compos_aidl_interface::binder::ProcessState;
+use binder::ProcessState;
 use compos_common::compos_client::{ComposClient, VmParameters};
 use compos_common::odrefresh::{
     CURRENT_ARTIFACTS_SUBDIR, ODREFRESH_OUTPUT_ROOT_DIR, PENDING_ARTIFACTS_SUBDIR,
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index b6ccd9e..fb6a1ad 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -66,7 +66,7 @@
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
     });
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log))
+    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
         .context("Failed to create VM")?;
 
     vm.start().context("Failed to start VM")?;
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index b46d5f8..acd0863 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -34,7 +34,7 @@
     VirtualMachineRawConfig::VirtualMachineRawConfig,
     VirtualMachineState::VirtualMachineState,
 };
-use android_system_virtualizationservice::binder::{
+use binder::{
     self, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor, SpIBinder, Status,
     StatusCode, Strong, ThreadState,
 };
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 95c5c53..3e1a151 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -36,7 +36,7 @@
 use std::thread;
 use vsock::VsockStream;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DeathReason::DeathReason;
-use android_system_virtualmachineservice::binder::Strong;
+use binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 7bfb531..3b0adb9 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -22,7 +22,7 @@
 
 use crate::aidl::{VirtualizationService, BINDER_SERVICE_IDENTIFIER, TEMPORARY_DIRECTORY};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use android_system_virtualizationservice::binder::{register_lazy_service, BinderFeatures, ProcessState};
+use binder::{register_lazy_service, BinderFeatures, ProcessState};
 use anyhow::Error;
 use log::{info, Level};
 use std::fs::{remove_dir_all, remove_file, read_dir};
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index c0153b9..42c51a1 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -19,9 +19,8 @@
     VirtualMachineAppConfig::VirtualMachineAppConfig,
     VirtualMachineRawConfig::VirtualMachineRawConfig,
 };
-use android_system_virtualizationservice::binder::ParcelFileDescriptor;
 use anyhow::{anyhow, bail, Context, Result};
-use binder::wait_for_interface;
+use binder::{wait_for_interface, ParcelFileDescriptor};
 use log::{info, warn};
 use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
diff --git a/vm/Android.bp b/vm/Android.bp
index 2b83ca7..f9eac4d 100644
--- a/vm/Android.bp
+++ b/vm/Android.bp
@@ -11,6 +11,7 @@
     rustlibs: [
         "android.system.virtualizationservice-rust",
         "libanyhow",
+        "libbinder_rs",
         "libenv_logger",
         "liblibc",
         "liblog_rust",
diff --git a/vm/src/create_idsig.rs b/vm/src/create_idsig.rs
index fe7cd82..9a66228 100644
--- a/vm/src/create_idsig.rs
+++ b/vm/src/create_idsig.rs
@@ -15,7 +15,7 @@
 //! Command to create or update an idsig for APK
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
-use android_system_virtualizationservice::binder::ParcelFileDescriptor;
+use binder::ParcelFileDescriptor;
 use anyhow::{Context, Error};
 use std::fs::{File, OpenOptions};
 use std::path::Path;
diff --git a/vm/src/create_partition.rs b/vm/src/create_partition.rs
index 049c201..affa28e 100644
--- a/vm/src/create_partition.rs
+++ b/vm/src/create_partition.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::PartitionType::PartitionType;
-use android_system_virtualizationservice::binder::ParcelFileDescriptor;
+use binder::ParcelFileDescriptor;
 use anyhow::{Context, Error};
 use std::convert::TryInto;
 use std::fs::OpenOptions;
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 60786ac..c421b04 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -22,8 +22,8 @@
     IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
     VirtualMachineAppConfig::DebugLevel::DebugLevel,
 };
-use android_system_virtualizationservice::binder::ProcessState;
 use anyhow::{Context, Error};
+use binder::ProcessState;
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app};
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 44eb27a..9bd7863 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -16,19 +16,13 @@
 
 use crate::create_partition::command_create_partition;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
-    DeathReason::DeathReason,
-    IVirtualMachineCallback::{BnVirtualMachineCallback, IVirtualMachineCallback},
-    IVirtualizationService::IVirtualizationService,
-    PartitionType::PartitionType,
+    IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
     VirtualMachineAppConfig::DebugLevel::DebugLevel,
-    VirtualMachineAppConfig::VirtualMachineAppConfig,
-    VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachineAppConfig::VirtualMachineAppConfig, VirtualMachineConfig::VirtualMachineConfig,
     VirtualMachineState::VirtualMachineState,
 };
-use android_system_virtualizationservice::binder::{
-    BinderFeatures, Interface, ParcelFileDescriptor, Result as BinderResult,
-};
 use anyhow::{bail, Context, Error};
+use binder::ParcelFileDescriptor;
 use microdroid_payload_config::VmPayloadConfig;
 use std::fs::File;
 use std::io::{self, BufRead, BufReader};
@@ -197,10 +191,9 @@
         Some(duplicate_stdout()?)
     };
 
-    let vm = VmInstance::create(service, config, console, log).context("Failed to create VM")?;
-    let callback =
-        BnVirtualMachineCallback::new_binder(VirtualMachineCallback {}, BinderFeatures::default());
-    vm.vm.registerCallback(&callback)?;
+    let callback = Box::new(Callback {});
+    let vm = VmInstance::create(service, config, console, log, Some(callback))
+        .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
 
     println!(
@@ -246,20 +239,13 @@
     Ok(config.extra_apks.into_iter().map(|x| x.path).collect())
 }
 
-#[derive(Debug)]
-struct VirtualMachineCallback {}
+struct Callback {}
 
-impl Interface for VirtualMachineCallback {}
-
-impl IVirtualMachineCallback for VirtualMachineCallback {
-    fn onPayloadStarted(
-        &self,
-        _cid: i32,
-        stream: Option<&ParcelFileDescriptor>,
-    ) -> BinderResult<()> {
+impl vmclient::VmCallback for Callback {
+    fn on_payload_started(&self, _cid: i32, stream: Option<&File>) {
         // Show the output of the payload
         if let Some(stream) = stream {
-            let mut reader = BufReader::new(stream.as_ref());
+            let mut reader = BufReader::new(stream);
             loop {
                 let mut s = String::new();
                 match reader.read_line(&mut s) {
@@ -269,31 +255,18 @@
                 };
             }
         }
-        Ok(())
     }
 
-    fn onPayloadReady(&self, _cid: i32) -> BinderResult<()> {
+    fn on_payload_ready(&self, _cid: i32) {
         eprintln!("payload is ready");
-        Ok(())
     }
 
-    fn onPayloadFinished(&self, _cid: i32, exit_code: i32) -> BinderResult<()> {
+    fn on_payload_finished(&self, _cid: i32, exit_code: i32) {
         eprintln!("payload finished with exit code {}", exit_code);
-        Ok(())
     }
 
-    fn onError(&self, _cid: i32, error_code: i32, message: &str) -> BinderResult<()> {
+    fn on_error(&self, _cid: i32, error_code: i32, message: &str) {
         eprintln!("VM encountered an error: code={}, message={}", error_code, message);
-        Ok(())
-    }
-
-    fn onRamdump(&self, _cid: i32, _stream: &ParcelFileDescriptor) -> BinderResult<()> {
-        // Do nothing. We get ramdump from the vmclient library.
-        Ok(())
-    }
-
-    fn onDied(&self, _cid: i32, _reason: DeathReason) -> BinderResult<()> {
-        Ok(())
     }
 }
 
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 4928846..fd6eb8c 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -63,7 +63,7 @@
     });
     let console = duplicate_stdout()?;
     let log = duplicate_stdout()?;
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log))
+    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
         .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
     info!("Started example VM.");
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index b3bb635..129e6c3 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -64,6 +64,32 @@
     _death_recipient: DeathRecipient,
 }
 
+/// A trait to be implemented by clients to handle notification of significant changes to the VM
+/// state. Default implementations of all functions are provided so clients only need to handle the
+/// notifications they are interested in.
+#[allow(unused_variables)]
+pub trait VmCallback {
+    /// Called when the payload has been started within the VM. If present, `stream` is connected
+    /// to the stdin/stdout of the payload.
+    fn on_payload_started(&self, cid: i32, stream: Option<&File>) {}
+
+    /// Callend when the payload has notified Virtualization Service that it is ready to serve
+    /// clients.
+    fn on_payload_ready(&self, cid: i32) {}
+
+    /// Called when the payload has exited in the VM. `exit_code` is the exit code of the payload
+    /// process.
+    fn on_payload_finished(&self, cid: i32, exit_code: i32) {}
+
+    /// Called when an error has occurred in the VM. The `error_code` and `message` may give
+    /// further details.
+    fn on_error(&self, cid: i32, error_code: i32, message: &str) {}
+
+    /// Called when the VM has exited, all resources have been freed, and any logs have been
+    /// written. `death_reason` gives an indication why the VM exited.
+    fn on_died(&self, cid: i32, death_reason: DeathReason) {}
+}
+
 impl VmInstance {
     /// Creates (but doesn't start) a new VM with the given configuration.
     pub fn create(
@@ -71,6 +97,7 @@
         config: &VirtualMachineConfig,
         console: Option<File>,
         log: Option<File>,
+        callback: Option<Box<dyn VmCallback + Send + Sync>>,
     ) -> BinderResult<Self> {
         let console = console.map(ParcelFileDescriptor::new);
         let log = log.map(ParcelFileDescriptor::new);
@@ -82,7 +109,7 @@
         // Register callback before starting VM, in case it dies immediately.
         let state = Arc::new(Monitor::new(VmState::default()));
         let callback = BnVirtualMachineCallback::new_binder(
-            VirtualMachineCallback { state: state.clone() },
+            VirtualMachineCallback { state: state.clone(), client_callback: callback },
             BinderFeatures::default(),
         );
         vm.registerCallback(&callback)?;
@@ -219,9 +246,21 @@
     }
 }
 
-#[derive(Debug)]
 struct VirtualMachineCallback {
     state: Arc<Monitor<VmState>>,
+    client_callback: Option<Box<dyn VmCallback + Send + Sync>>,
+}
+
+impl Debug for VirtualMachineCallback {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        fmt.debug_struct("VirtualMachineCallback")
+            .field("state", &self.state)
+            .field(
+                "client_callback",
+                &if self.client_callback.is_some() { "Some(...)" } else { "None" },
+            )
+            .finish()
+    }
 }
 
 impl Interface for VirtualMachineCallback {}
@@ -229,25 +268,37 @@
 impl IVirtualMachineCallback for VirtualMachineCallback {
     fn onPayloadStarted(
         &self,
-        _cid: i32,
-        _stream: Option<&ParcelFileDescriptor>,
+        cid: i32,
+        stream: Option<&ParcelFileDescriptor>,
     ) -> BinderResult<()> {
         self.state.notify_state(VirtualMachineState::STARTED);
+        if let Some(ref callback) = self.client_callback {
+            callback.on_payload_started(cid, stream.map(ParcelFileDescriptor::as_ref));
+        }
         Ok(())
     }
 
-    fn onPayloadReady(&self, _cid: i32) -> BinderResult<()> {
+    fn onPayloadReady(&self, cid: i32) -> BinderResult<()> {
         self.state.notify_state(VirtualMachineState::READY);
+        if let Some(ref callback) = self.client_callback {
+            callback.on_payload_ready(cid);
+        }
         Ok(())
     }
 
-    fn onPayloadFinished(&self, _cid: i32, _exit_code: i32) -> BinderResult<()> {
+    fn onPayloadFinished(&self, cid: i32, exit_code: i32) -> BinderResult<()> {
         self.state.notify_state(VirtualMachineState::FINISHED);
+        if let Some(ref callback) = self.client_callback {
+            callback.on_payload_finished(cid, exit_code);
+        }
         Ok(())
     }
 
-    fn onError(&self, _cid: i32, _error_code: i32, _message: &str) -> BinderResult<()> {
+    fn onError(&self, cid: i32, error_code: i32, message: &str) -> BinderResult<()> {
         self.state.notify_state(VirtualMachineState::FINISHED);
+        if let Some(ref callback) = self.client_callback {
+            callback.on_error(cid, error_code, message);
+        }
         Ok(())
     }
 
@@ -257,8 +308,12 @@
         Ok(())
     }
 
-    fn onDied(&self, _cid: i32, reason: AidlDeathReason) -> BinderResult<()> {
-        self.state.notify_death(reason.into());
+    fn onDied(&self, cid: i32, reason: AidlDeathReason) -> BinderResult<()> {
+        let reason = reason.into();
+        self.state.notify_death(reason);
+        if let Some(ref callback) = self.client_callback {
+            callback.on_died(cid, reason);
+        }
         Ok(())
     }
 }