Skip to content

Commit 99d3d88

Browse files
authored
[workerd-cxx] Add kj::Own ffi type to workerd-cxx (#26)
Extends the cxx bridge module to allow using the `Own` smart pointer in Rust from C++ code. Using a static disposer is not supported in Rust. Passing a null Own to Rust, through returns or function arguments, will result in an exception. Tests contain both tests that were hand-written and generated by Claude Code.
1 parent 832fbf0 commit 99d3d88

File tree

21 files changed

+749
-5
lines changed

21 files changed

+749
-5
lines changed

gen/src/write.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ fn pick_includes_and_builtins(out: &mut OutFile, apis: &[Api]) {
223223
Type::SliceRef(_) => out.builtin.rust_slice = true,
224224
Type::Array(_) => out.include.array = true,
225225
Type::Ref(_) | Type::Void(_) | Type::Ptr(_) => {}
226-
Type::Future(_) => out.include.kj_rs = true,
226+
Type::Future(_) | Type::Own(_) => out.include.kj_rs = true,
227227
}
228228
}
229229
}
@@ -1030,6 +1030,12 @@ fn write_rust_function_shim_impl(
10301030
return;
10311031
}
10321032
writeln!(out, " {{");
1033+
sig.args.iter()
1034+
.filter(|arg| {
1035+
matches!(arg.ty, Type::Own(_))
1036+
}).for_each(|arg_own| {
1037+
writeln!(out, " KJ_ASSERT({}.get() != nullptr, \"Cannot pass a null Own to Rust\");", arg_own.name.cxx);
1038+
});
10331039
for arg in &sig.args {
10341040
if arg.ty != RustString && out.types.needs_indirect_abi(&arg.ty) {
10351041
out.include.utility = true;
@@ -1228,6 +1234,11 @@ fn write_type(out: &mut OutFile, ty: &Type) {
12281234
write_type(out, &ptr.inner);
12291235
write!(out, ">");
12301236
}
1237+
Type::Own(ptr) => {
1238+
write!(out, "::kj::Own<");
1239+
write_type(out, &ptr.inner);
1240+
write!(out, ">");
1241+
}
12311242
Type::SharedPtr(ptr) => {
12321243
write!(out, "::std::shared_ptr<");
12331244
write_type(out, &ptr.inner);
@@ -1328,6 +1339,7 @@ fn write_space_after_type(out: &mut OutFile, ty: &Type) {
13281339
Type::Ident(_)
13291340
| Type::RustBox(_)
13301341
| Type::UniquePtr(_)
1342+
| Type::Own(_)
13311343
| Type::SharedPtr(_)
13321344
| Type::WeakPtr(_)
13331345
| Type::Str(_)
@@ -1404,6 +1416,7 @@ fn write_generic_instantiations(out: &mut OutFile) {
14041416
ImplKey::RustBox(ident) => write_rust_box_extern(out, ident),
14051417
ImplKey::RustVec(ident) => write_rust_vec_extern(out, ident),
14061418
ImplKey::UniquePtr(ident) => write_unique_ptr(out, ident),
1419+
ImplKey::Own(ident) => write_kj_own(out, ident),
14071420
ImplKey::SharedPtr(ident) => write_shared_ptr(out, ident),
14081421
ImplKey::WeakPtr(ident) => write_weak_ptr(out, ident),
14091422
ImplKey::CxxVector(ident) => write_cxx_vector(out, ident),
@@ -1615,6 +1628,34 @@ fn write_rust_vec_impl(out: &mut OutFile, key: NamedImplKey) {
16151628
writeln!(out, "}}");
16161629
}
16171630

1631+
// Writes static assertion that we do not use an Own with a static disposer
1632+
fn write_kj_own(out: &mut OutFile, key: NamedImplKey) {
1633+
let ident = key.rust;
1634+
let resolve = out.types.resolve(ident);
1635+
let inner = resolve.name.to_fully_qualified();
1636+
let instance = resolve.name.to_symbol();
1637+
1638+
out.include.utility = true;
1639+
out.include.kj_rs = true;
1640+
1641+
// Static disposers are not supported, only Owns containing 2 pointers are allowed
1642+
writeln!(
1643+
out,
1644+
"static_assert(sizeof(::kj::Own<{}>) == 2 * sizeof(void *), \"Static disposers for Own are not supported in workerd-cxx\");",
1645+
inner,
1646+
);
1647+
writeln!(
1648+
out,
1649+
"static_assert(alignof(::kj::Own<{}>) == sizeof(void *), \"Static disposers for Own are not supported in workerd-cxx\");",
1650+
inner,
1651+
);
1652+
writeln!(
1653+
out,
1654+
"static_assert(!::std::is_base_of<::kj::Refcounted, {}>::value, \"Value must not inherit from kj::Refcounted\");",
1655+
inner
1656+
);
1657+
}
1658+
16181659
fn write_unique_ptr(out: &mut OutFile, key: NamedImplKey) {
16191660
let ty = UniquePtr::Ident(key.rust);
16201661
write_unique_ptr_common(out, ty);

kj-rs/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ mod awaiter;
1414
mod future;
1515
mod promise;
1616
mod waker;
17+
mod own;
1718

1819
pub mod repr {
1920
pub use crate::future::repr::*;
21+
pub use crate::own::repr::*;
2022
}
2123

2224
pub type Result<T> = std::io::Result<T>;

kj-rs/own.c++

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#include "own.h"
2+
3+
extern "C" {
4+
// As of right now, this function works and passes all tests, destroying all tested objects correctly.
5+
// This works because disposers work by virtual call, and the disposer is created during creation of
6+
// the Own, so the type of the Own at destruction doesn't actually matter for Owns that do not use a
7+
// static disposer, which are not currently supported by workerd-cxx.
8+
void cxxbridge$kjrs$own$drop(void *own) {
9+
reinterpret_cast<kj::Own<void> *>(own)->~Own();
10+
}
11+
}

kj-rs/own.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
3+
#include <kj/memory.h>
4+
5+
extern "C" {
6+
void cxxbridge$kjrs$own$drop(void* own);
7+
}

kj-rs/own.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! The `workerd-cxx` module containing the [`Own<T>`] type, which is bindings to the `kj::Own<T>` C++ type
2+
3+
use static_assertions::{assert_eq_align, assert_eq_size};
4+
5+
assert_eq_size!(repr::Own<()>, [*const (); 2]);
6+
assert_eq_align!(repr::Own<()>, *const ());
7+
8+
pub mod repr {
9+
use std::ffi::c_void;
10+
use std::fmt::{self, Debug, Display};
11+
use std::hash::{Hash, Hasher};
12+
use std::ops::Deref;
13+
use std::ops::DerefMut;
14+
use std::pin::Pin;
15+
use std::ptr::NonNull;
16+
17+
/// A [`Own<T>`] represents the `kj::Own<T>`. It is a smart pointer to an opaque C++ type.
18+
/// Safety:
19+
/// - Passing a null `kj::Own` to rust is considered unsafe from the C++ side,
20+
/// and it is required that this invariant is upheld in C++ code.
21+
/// - Currently, it is runtime asserted in the bridge macro that no null Own can be passed
22+
/// to Rust
23+
#[repr(C)]
24+
pub struct Own<T> {
25+
disposer: *const c_void,
26+
ptr: NonNull<T>,
27+
}
28+
29+
/// Public-facing Own api
30+
impl<T> Own<T> {
31+
/// Returns a mutable pinned reference to the object owned by this [`Own`]
32+
/// if any, otherwise None.
33+
pub fn as_mut(&mut self) -> Pin<&mut T> {
34+
// Safety: Passing a null kj::Own to Rust from C++ is not supported.
35+
unsafe {
36+
let mut_reference = self.ptr.as_mut();
37+
Pin::new_unchecked(mut_reference)
38+
}
39+
}
40+
41+
/// Returns a mutable pinned reference to the object owned by this
42+
/// [`Own`].
43+
///
44+
/// ```compile_fail
45+
/// let mut own = ffi::cxx_kj_own();
46+
/// let pin1 = own.pin_mut();
47+
/// let pin2 = own.pin_mut();
48+
/// pin1.set_data(12); // Causes a compile fail, because we invalidated the first borrow
49+
/// ```
50+
///
51+
/// ```compile_fail
52+
///
53+
/// let mut own = ffi::cxx_kj_own();
54+
/// let pin = own.pin_mut();
55+
/// let moved = own;
56+
/// own.set_data(143); // Compile fail, because we tried using a moved object
57+
/// ```
58+
pub fn pin_mut(&mut self) -> Pin<&mut T> {
59+
self.as_mut()
60+
}
61+
62+
/// Returns a raw const pointer to the object owned by this [`Own`]
63+
#[must_use]
64+
pub fn as_ptr(&self) -> *const T {
65+
self.ptr.as_ptr().cast()
66+
}
67+
}
68+
69+
impl<T> AsRef<T> for Own<T> {
70+
/// Returns a reference to the object owned by this [`Own`] if any,
71+
/// otherwise None.
72+
fn as_ref(&self) -> &T {
73+
// Safety: Passing a null kj::Own to Rust from C++ is not supported.
74+
unsafe { self.ptr.as_ref() }
75+
}
76+
}
77+
78+
unsafe impl<T> Send for Own<T> where T: Send {}
79+
80+
unsafe impl<T> Sync for Own<T> where T: Sync {}
81+
82+
impl<T> Deref for Own<T> {
83+
type Target = T;
84+
85+
fn deref(&self) -> &Self::Target {
86+
self.as_ref()
87+
}
88+
}
89+
90+
impl<T> DerefMut for Own<T>
91+
where
92+
T: Unpin,
93+
{
94+
fn deref_mut(&mut self) -> &mut Self::Target {
95+
Pin::into_inner(self.as_mut())
96+
}
97+
}
98+
99+
// Own<T> is safe to implement Unpin because moving the Own doesn't move the pointee, and
100+
// the drop implentation doesn't depend on the Own's location, because it's handed by virtual dispatch
101+
impl<T> Unpin for Own<T> {}
102+
103+
impl<T> Debug for Own<T> {
104+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105+
write!(f, "Own(ptr: {:p}, disposer: {:p})", self.ptr, self.disposer)
106+
}
107+
}
108+
109+
impl<T> Display for Own<T>
110+
where
111+
T: Display,
112+
{
113+
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
114+
Display::fmt(self.as_ref(), formatter)
115+
}
116+
}
117+
118+
impl<T> PartialEq for Own<T>
119+
where
120+
T: PartialEq,
121+
{
122+
fn eq(&self, other: &Self) -> bool {
123+
self.as_ref() == other.as_ref()
124+
}
125+
}
126+
127+
impl<T> Eq for Own<T> where T: Eq {}
128+
129+
impl<T> PartialOrd for Own<T>
130+
where
131+
T: PartialOrd,
132+
{
133+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
134+
PartialOrd::partial_cmp(&self.as_ref(), &other.as_ref())
135+
}
136+
}
137+
138+
impl<T> Ord for Own<T>
139+
where
140+
T: Ord,
141+
{
142+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
143+
Ord::cmp(&self.as_ref(), &other.as_ref())
144+
}
145+
}
146+
147+
impl<T> Hash for Own<T>
148+
where
149+
T: Hash,
150+
{
151+
fn hash<H: Hasher>(&self, state: &mut H) {
152+
self.as_ref().hash(state);
153+
}
154+
}
155+
156+
impl<T> Drop for Own<T> {
157+
fn drop(&mut self) {
158+
unsafe extern "C" {
159+
#[link_name = "cxxbridge$kjrs$own$drop"]
160+
fn __drop(this: *mut c_void);
161+
}
162+
163+
let this = std::ptr::from_mut::<Self>(self).cast::<c_void>();
164+
unsafe {
165+
__drop(this);
166+
}
167+
}
168+
}
169+
}

kj-rs/tests/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ rust_cxx_bridge(
4545
src = "lib.rs",
4646
hdrs = [
4747
"test-promises.h",
48+
"test-own.h"
4849
],
4950
include_prefix = "kj-rs-demo",
5051
deps = [
@@ -56,6 +57,7 @@ cc_library(
5657
name = "test-promises",
5758
srcs = [
5859
"test-promises.c++",
60+
"test-own.c++"
5961
],
6062
linkstatic = select({
6163
"@platforms//os:windows": True,

kj-rs/tests/lib.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
#![allow(clippy::needless_lifetimes)]
22
#![allow(clippy::missing_errors_doc)]
33
#![allow(clippy::unused_async)]
4+
#![allow(clippy::must_use_candidate)]
5+
#![allow(clippy::cast_possible_truncation)]
6+
#![allow(clippy::should_panic_without_expect)]
7+
#![allow(clippy::missing_panics_doc)]
48

59
mod test_futures;
10+
mod test_own;
611

712
use test_futures::{
813
new_awaiting_future_i32, new_error_handling_future_void_infallible, new_errored_future_void,
@@ -11,6 +16,8 @@ use test_futures::{
1116
new_waking_future_void, new_wrapped_waker_future_void,
1217
};
1318

19+
use kj_rs::repr::Own;
20+
1421
type Result<T> = std::io::Result<T>;
1522
type Error = std::io::Error;
1623

@@ -32,6 +39,37 @@ mod ffi {
3239
async fn new_ready_promise_shared_type() -> Shared;
3340
}
3441

42+
// Helper functions to test `kj_rs::Own`
43+
unsafe extern "C++" {
44+
include!("kj-rs-demo/test-own.h");
45+
type OpaqueCxxClass;
46+
47+
#[cxx_name = "getData"]
48+
fn get_data(&self) -> u64;
49+
#[cxx_name = "setData"]
50+
fn set_data(self: Pin<&mut OpaqueCxxClass>, val: u64);
51+
52+
fn cxx_kj_own() -> Own<OpaqueCxxClass>;
53+
fn null_kj_own() -> Own<OpaqueCxxClass>;
54+
fn give_own_back(own: Own<OpaqueCxxClass>);
55+
fn modify_own_return_test();
56+
fn breaking_things() -> Own<OpaqueCxxClass>;
57+
58+
fn own_integer() -> Own<i64>;
59+
fn own_integer_attached() -> Own<i64>;
60+
61+
fn null_exception_test_driver_1() -> String;
62+
fn null_exception_test_driver_2() -> String;
63+
fn rust_take_own_driver();
64+
}
65+
66+
// Helper function to test moving `Own` to C++
67+
extern "Rust" {
68+
fn modify_own_return(cpp_own: Own<OpaqueCxxClass>) -> Own<OpaqueCxxClass>;
69+
fn take_own(cpp_own: Own<OpaqueCxxClass>);
70+
fn get_null() -> Own<OpaqueCxxClass>;
71+
}
72+
3573
enum CloningAction {
3674
None,
3775
CloneSameThread,
@@ -75,6 +113,22 @@ mod ffi {
75113
}
76114
}
77115

116+
pub fn modify_own_return(mut own: Own<ffi::OpaqueCxxClass>) -> Own<ffi::OpaqueCxxClass> {
117+
own.pin_mut().set_data(72);
118+
own
119+
}
120+
121+
pub fn get_null() -> Own<ffi::OpaqueCxxClass> {
122+
ffi::null_kj_own()
123+
}
124+
125+
pub fn take_own(cpp_own: Own<ffi::OpaqueCxxClass>) {
126+
assert_eq!(cpp_own.get_data(), 14);
127+
// The point of this function is to drop the [`Own`] from rust and this makes
128+
// it explicit, while avoiding a clippy lint
129+
std::mem::drop(cpp_own);
130+
}
131+
78132
pub async fn lifetime_arg_void<'a>(_buf: &'a [u8]) {}
79133

80134
pub async fn lifetime_arg_result<'a>(_buf: &'a [u8]) -> Result<()> {

0 commit comments

Comments
 (0)