From fd5dd095c706a3dcb04ed7da7000ec7422c698e1 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Fri, 29 May 2026 20:32:44 -0700 Subject: [PATCH 01/18] feat: create tree sequence from a raw pointer --- src/sys/treeseq.rs | 9 +++++++++ src/trees/treeseq.rs | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/sys/treeseq.rs b/src/sys/treeseq.rs index 0a78eaa9..f74d83c1 100644 --- a/src/sys/treeseq.rs +++ b/src/sys/treeseq.rs @@ -46,6 +46,15 @@ impl TreeSequence { Ok(Self(tsk)) } + // # Safety + // + // `tables` must be an initialized `tsk_table_collection_t` + pub unsafe fn new_owning_from_nonnull( + treeseq: std::ptr::NonNull, + ) -> Self { + Self(TskBox::new_init_owning_from_ptr(treeseq)) + } + pub fn as_ref(&self) -> &bindings::tsk_treeseq_t { self.0.as_ref() } diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index 596056e6..48cadfeb 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -732,6 +732,19 @@ impl TreeSequence { ) -> impl crate::TableColumn + '_ { crate::table_column::OpaqueTableColumn(self.edge_removal_order()) } + + #[cfg(feature = "unsafe_init")] + pub unsafe fn new_from_raw( + ptr: std::ptr::NonNull, + ) -> Result { + let tables = unsafe { + TableCollection::new_from_ll(sys::TableCollection::new_borrowed( + std::ptr::NonNull::new(ptr.as_ref().tables).unwrap(), + )) + }?; + let inner = sys::TreeSequence::new(tables, 0.into())?; + Ok(Self{inner, tables }) + } } impl TryFrom for TreeSequence { From 927eac9d34d560307bb09715880d5fa2cd25f945 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 07:52:52 -0700 Subject: [PATCH 02/18] correct the API use --- src/trees/treeseq.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index 48cadfeb..7bd7279b 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -737,13 +737,16 @@ impl TreeSequence { pub unsafe fn new_from_raw( ptr: std::ptr::NonNull, ) -> Result { + let ll_tables = sys::TableCollection::new_owning_from_nonnull( + std::ptr::NonNull::new(ptr.as_ref().tables).unwrap(), + ); let tables = unsafe { TableCollection::new_from_ll(sys::TableCollection::new_borrowed( std::ptr::NonNull::new(ptr.as_ref().tables).unwrap(), )) }?; - let inner = sys::TreeSequence::new(tables, 0.into())?; - Ok(Self{inner, tables }) + let inner = sys::TreeSequence::new(ll_tables, 0.into())?; + Ok(Self { inner, tables }) } } From 647e82d968cc81b6d5ed2c1ac5b348f0ea6e6ae9 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 07:56:25 -0700 Subject: [PATCH 03/18] fmt --- src/sys/treeseq.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sys/treeseq.rs b/src/sys/treeseq.rs index f74d83c1..0623c771 100644 --- a/src/sys/treeseq.rs +++ b/src/sys/treeseq.rs @@ -49,9 +49,7 @@ impl TreeSequence { // # Safety // // `tables` must be an initialized `tsk_table_collection_t` - pub unsafe fn new_owning_from_nonnull( - treeseq: std::ptr::NonNull, - ) -> Self { + pub unsafe fn new_owning_from_nonnull(treeseq: std::ptr::NonNull) -> Self { Self(TskBox::new_init_owning_from_ptr(treeseq)) } From 87b72d37bb3071a805dae5c77df73d82df01477f Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 07:57:33 -0700 Subject: [PATCH 04/18] fix comments --- src/sys/treeseq.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sys/treeseq.rs b/src/sys/treeseq.rs index 0623c771..56add2e9 100644 --- a/src/sys/treeseq.rs +++ b/src/sys/treeseq.rs @@ -48,7 +48,7 @@ impl TreeSequence { // # Safety // - // `tables` must be an initialized `tsk_table_collection_t` + // `treeseq` must be an initialized `tsk_treeseq_t_t` pub unsafe fn new_owning_from_nonnull(treeseq: std::ptr::NonNull) -> Self { Self(TskBox::new_init_owning_from_ptr(treeseq)) } From ae19b3f2e868007384fffcf45716c64b88b4a13c Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 07:59:09 -0700 Subject: [PATCH 05/18] use new fn --- src/trees/treeseq.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index 7bd7279b..aa31353b 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -737,15 +737,12 @@ impl TreeSequence { pub unsafe fn new_from_raw( ptr: std::ptr::NonNull, ) -> Result { - let ll_tables = sys::TableCollection::new_owning_from_nonnull( - std::ptr::NonNull::new(ptr.as_ref().tables).unwrap(), - ); let tables = unsafe { TableCollection::new_from_ll(sys::TableCollection::new_borrowed( std::ptr::NonNull::new(ptr.as_ref().tables).unwrap(), )) }?; - let inner = sys::TreeSequence::new(ll_tables, 0.into())?; + let inner = sys::TreeSequence::new_owning_from_nonnull(ptr); Ok(Self { inner, tables }) } } From 8ba3948758daca82090348bd52657d3136b73096 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 08:07:19 -0700 Subject: [PATCH 06/18] ts into raw --- src/sys/treeseq.rs | 4 ++++ src/trees/treeseq.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/sys/treeseq.rs b/src/sys/treeseq.rs index 56add2e9..1bc01a6e 100644 --- a/src/sys/treeseq.rs +++ b/src/sys/treeseq.rs @@ -46,6 +46,10 @@ impl TreeSequence { Ok(Self(tsk)) } + pub fn into_raw(self) -> *mut tsk_treeseq_t { + self.0.into_raw() + } + // # Safety // // `treeseq` must be an initialized `tsk_treeseq_t_t` diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index aa31353b..578e1173 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -1,3 +1,4 @@ +use crate::bindings::tsk_treeseq_t; use crate::error::TskitError; use crate::sys; use crate::EdgeTable; @@ -745,6 +746,10 @@ impl TreeSequence { let inner = sys::TreeSequence::new_owning_from_nonnull(ptr); Ok(Self { inner, tables }) } + + pub fn into_mut_ptr(self) -> Option> { + std::ptr::NonNull::new(self.inner.into_raw()) + } } impl TryFrom for TreeSequence { From 3ceca0231949fa5b3ba0ca2e596aaee2a244ec7e Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:24:03 -0700 Subject: [PATCH 07/18] need some kind of python side test --- python/zerocopy/Cargo.lock | 2 +- python/zerocopy/src/lib.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/python/zerocopy/Cargo.lock b/python/zerocopy/Cargo.lock index 330c59e9..585b1e68 100644 --- a/python/zerocopy/Cargo.lock +++ b/python/zerocopy/Cargo.lock @@ -403,7 +403,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tskit" -version = "0.15.0-alpha.4" +version = "0.16.3" dependencies = [ "bindgen", "cc", diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index fa1bb4f1..5eea044f 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -73,3 +73,12 @@ fn treeseq_roundtrip() { unsafe { pyo3::ffi::PyMem_Free(tables_ptr.as_ptr().cast::()) }; }); } + +#[test] +fn test_treeseq_sharing() { + use pyo3::prelude::*; + + Python::attach(|_py| { + todo!() + }) +} From 34c35d180199ea3ff4b9d9f46fc2d3009330fb20 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:27:54 -0700 Subject: [PATCH 08/18] fix error --- src/trees/treeseq.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index 578e1173..fa52d298 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -1,4 +1,3 @@ -use crate::bindings::tsk_treeseq_t; use crate::error::TskitError; use crate::sys; use crate::EdgeTable; @@ -19,6 +18,7 @@ use crate::TreeFlags; use crate::TreeSequenceFlags; use crate::TskReturnValue; use sys::bindings as ll_bindings; +use sys::bindings::tsk_treeseq_t; #[cfg(feature = "provenance")] use std::ffi::c_char; From ce478205b7d1adfabf62edf96a2878676028844e Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:38:42 -0700 Subject: [PATCH 09/18] rust-side test --- src/trees/treeseq.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/trees/treeseq.rs b/src/trees/treeseq.rs index fa52d298..1d1114b3 100644 --- a/src/trees/treeseq.rs +++ b/src/trees/treeseq.rs @@ -759,3 +759,24 @@ impl TryFrom for TreeSequence { Self::new(value, TreeSequenceFlags::default()) } } + +#[cfg(feature = "unsafe_init")] +#[test] +fn test_new_from_raw() { + let mut tables = crate::TableCollection::new(100.).unwrap(); + tables.add_node(0, 0.0, -1, -1).unwrap(); + + let treeseq = unsafe { libc::malloc(std::mem::size_of::()) } + as *mut sys::bindings::tsk_treeseq_t; + let rv = unsafe { + sys::bindings::tsk_treeseq_init( + treeseq, + tables.into_mut_ptr().unwrap().as_ptr(), + sys::bindings::TSK_TAKE_OWNERSHIP | sys::bindings::TSK_TS_INIT_BUILD_INDEXES, + ) + }; + assert_eq!(rv, 0); + let ptr = std::ptr::NonNull::new(treeseq).unwrap(); + let rs_treeseq = unsafe { TreeSequence::new_from_raw(ptr) }.unwrap(); + assert_eq!(rs_treeseq.nodes().num_rows(), 1); +} From aedd20a88cef0ee73cf49b43609eca93b9680c74 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:46:31 -0700 Subject: [PATCH 10/18] py tests --- python/zerocopy/src/lib.rs | 54 +++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 5eea044f..7dfed87c 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -74,11 +74,59 @@ fn treeseq_roundtrip() { }); } +// Should panic because we are allocating with Python malloc +// by letting the tskit object drop, which attempts to +// free using libc::free. #[test] -fn test_treeseq_sharing() { +#[should_panic] +fn test_treeseq_new_from_raw_invalid() { use pyo3::prelude::*; + Python::attach(|_py| { + let mut tables = tskit::TableCollection::new(100.).unwrap(); + tables.add_node(0, 0.0, -1, -1).unwrap(); + + let treeseq = unsafe { + pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) + } as *mut tskit::bindings::tsk_treeseq_t; + let rv = unsafe { + tskit::bindings::tsk_treeseq_init( + treeseq, + tables.into_mut_ptr().unwrap().as_ptr(), + tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, + ) + }; + assert_eq!(rv, 0); + let ptr = std::ptr::NonNull::new(treeseq).unwrap(); + let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); + assert_eq!(rs_treeseq.nodes().num_rows(), 1); + }); +} +#[test] +#[should_panic] +fn test_treeseq_new_from_raw() { + use pyo3::prelude::*; Python::attach(|_py| { - todo!() - }) + let mut tables = tskit::TableCollection::new(100.).unwrap(); + tables.add_node(0, 0.0, -1, -1).unwrap(); + + let treeseq = unsafe { + pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) + } as *mut tskit::bindings::tsk_treeseq_t; + let rv = unsafe { + tskit::bindings::tsk_treeseq_init( + treeseq, + tables.into_mut_ptr().unwrap().as_ptr(), + tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, + ) + }; + assert_eq!(rv, 0); + let ptr = std::ptr::NonNull::new(treeseq).unwrap(); + let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); + assert_eq!(rs_treeseq.nodes().num_rows(), 1); + let ptr = rs_treeseq.into_mut_ptr().unwrap(); + unsafe { + pyo3::ffi::PyMem_Free(ptr.as_ptr() as *mut std::ffi::c_void); + } + }); } From 46a0f13a95e05fc6590c315fa3fdebb0d01bb13e Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:51:10 -0700 Subject: [PATCH 11/18] should not panic --- python/zerocopy/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 7dfed87c..1b70e116 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -103,7 +103,6 @@ fn test_treeseq_new_from_raw_invalid() { } #[test] -#[should_panic] fn test_treeseq_new_from_raw() { use pyo3::prelude::*; Python::attach(|_py| { From 8b1aa344bc8a27c6368180f490820c2de9671728 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Sun, 31 May 2026 12:56:37 -0700 Subject: [PATCH 12/18] Python tests pass --- python/zerocopy/src/lib.rs | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 1b70e116..1a06cf9d 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -77,30 +77,30 @@ fn treeseq_roundtrip() { // Should panic because we are allocating with Python malloc // by letting the tskit object drop, which attempts to // free using libc::free. -#[test] -#[should_panic] -fn test_treeseq_new_from_raw_invalid() { - use pyo3::prelude::*; - Python::attach(|_py| { - let mut tables = tskit::TableCollection::new(100.).unwrap(); - tables.add_node(0, 0.0, -1, -1).unwrap(); - - let treeseq = unsafe { - pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) - } as *mut tskit::bindings::tsk_treeseq_t; - let rv = unsafe { - tskit::bindings::tsk_treeseq_init( - treeseq, - tables.into_mut_ptr().unwrap().as_ptr(), - tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, - ) - }; - assert_eq!(rv, 0); - let ptr = std::ptr::NonNull::new(treeseq).unwrap(); - let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); - assert_eq!(rs_treeseq.nodes().num_rows(), 1); - }); -} +// #[test] +// #[should_panic] +// fn test_treeseq_new_from_raw_invalid() { +// use pyo3::prelude::*; +// Python::attach(|_py| { +// let mut tables = tskit::TableCollection::new(100.).unwrap(); +// tables.add_node(0, 0.0, -1, -1).unwrap(); +// +// let treeseq = unsafe { +// pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) +// } as *mut tskit::bindings::tsk_treeseq_t; +// let rv = unsafe { +// tskit::bindings::tsk_treeseq_init( +// treeseq, +// tables.into_mut_ptr().unwrap().as_ptr(), +// tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, +// ) +// }; +// assert_eq!(rv, 0); +// let ptr = std::ptr::NonNull::new(treeseq).unwrap(); +// let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); +// assert_eq!(rs_treeseq.nodes().num_rows(), 1); +// }); +// } #[test] fn test_treeseq_new_from_raw() { From 45187232cc6984d454b5e3924ff9c1753f7affae Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 13:37:21 -0700 Subject: [PATCH 13/18] remove commented out code --- python/zerocopy/src/lib.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 1a06cf9d..880c5d15 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -74,34 +74,6 @@ fn treeseq_roundtrip() { }); } -// Should panic because we are allocating with Python malloc -// by letting the tskit object drop, which attempts to -// free using libc::free. -// #[test] -// #[should_panic] -// fn test_treeseq_new_from_raw_invalid() { -// use pyo3::prelude::*; -// Python::attach(|_py| { -// let mut tables = tskit::TableCollection::new(100.).unwrap(); -// tables.add_node(0, 0.0, -1, -1).unwrap(); -// -// let treeseq = unsafe { -// pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) -// } as *mut tskit::bindings::tsk_treeseq_t; -// let rv = unsafe { -// tskit::bindings::tsk_treeseq_init( -// treeseq, -// tables.into_mut_ptr().unwrap().as_ptr(), -// tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, -// ) -// }; -// assert_eq!(rv, 0); -// let ptr = std::ptr::NonNull::new(treeseq).unwrap(); -// let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); -// assert_eq!(rs_treeseq.nodes().num_rows(), 1); -// }); -// } - #[test] fn test_treeseq_new_from_raw() { use pyo3::prelude::*; From fd0947d9ebb82d63b48f9394708d4100a10d8681 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 13:41:43 -0700 Subject: [PATCH 14/18] test fixes --- python/zerocopy/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 880c5d15..98cca90d 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -95,7 +95,53 @@ fn test_treeseq_new_from_raw() { let ptr = std::ptr::NonNull::new(treeseq).unwrap(); let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); assert_eq!(rs_treeseq.nodes().num_rows(), 1); - let ptr = rs_treeseq.into_mut_ptr().unwrap(); + let mut ptr = rs_treeseq.into_mut_ptr().unwrap(); + let rv = unsafe { tskit::bindings::tsk_treeseq_free(ptr.as_mut()) }; + assert_eq!(rv, 0); + unsafe { + pyo3::ffi::PyMem_Free(ptr.as_ptr() as *mut std::ffi::c_void); + } + }); +} + +#[test] +fn test_treeseq_new_from_raw_tables_also_py_allocated() { + use pyo3::prelude::*; + use tskit::bindings::tsk_table_collection_init; + use tskit::bindings::tsk_table_collection_t; + + Python::attach(|_py| { + let tables_ptr = unsafe { + pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) + .cast::() + }; + assert!(!tables_ptr.is_null()); + + // SAFETY: ptr is not null + let rv = unsafe { tsk_table_collection_init(tables_ptr, 0) }; + assert_eq!(rv, 0); + let tables_ptr = std::ptr::NonNull::new(tables_ptr).unwrap(); + // Not null and initialized w/o error + let mut tables = unsafe { tskit::TableCollection::new_from_raw(tables_ptr) }.unwrap(); + let _ = tables.add_node(0, 0.0, -1, -1).unwrap(); + + let treeseq = unsafe { + pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) + } as *mut tskit::bindings::tsk_treeseq_t; + let rv = unsafe { + tskit::bindings::tsk_treeseq_init( + treeseq, + tables.into_mut_ptr().unwrap().as_ptr(), + tskit::bindings::TSK_TAKE_OWNERSHIP | tskit::bindings::TSK_TS_INIT_BUILD_INDEXES, + ) + }; + assert_eq!(rv, 0); + let ptr = std::ptr::NonNull::new(treeseq).unwrap(); + let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); + assert_eq!(rs_treeseq.nodes().num_rows(), 1); + let mut ptr = rs_treeseq.into_mut_ptr().unwrap(); + let rv = unsafe { tskit::bindings::tsk_treeseq_free(ptr.as_mut()) }; + assert_eq!(rv, 0); unsafe { pyo3::ffi::PyMem_Free(ptr.as_ptr() as *mut std::ffi::c_void); } From 20d4ccb747f88f5299fae3288c50f5b4960fca37 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 13:47:55 -0700 Subject: [PATCH 15/18] test fixes --- python/zerocopy/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 98cca90d..58c5839a 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -119,11 +119,14 @@ fn test_treeseq_new_from_raw_tables_also_py_allocated() { // SAFETY: ptr is not null let rv = unsafe { tsk_table_collection_init(tables_ptr, 0) }; + unsafe { (*tables_ptr).sequence_length = 100.0 }; + assert_eq!(unsafe { *tables_ptr }.sequence_length, 100.); assert_eq!(rv, 0); let tables_ptr = std::ptr::NonNull::new(tables_ptr).unwrap(); // Not null and initialized w/o error let mut tables = unsafe { tskit::TableCollection::new_from_raw(tables_ptr) }.unwrap(); let _ = tables.add_node(0, 0.0, -1, -1).unwrap(); + assert_eq!(tables.sequence_length(), 100.); let treeseq = unsafe { pyo3::ffi::PyMem_Malloc(std::mem::size_of::()) From 69298a28144749738feb1cd9024740dfc513989f Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 13:55:21 -0700 Subject: [PATCH 16/18] that is tricky --- python/zerocopy/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 58c5839a..e15155da 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -143,6 +143,17 @@ fn test_treeseq_new_from_raw_tables_also_py_allocated() { let rs_treeseq = unsafe { tskit::TreeSequence::new_from_raw(ptr) }.unwrap(); assert_eq!(rs_treeseq.nodes().num_rows(), 1); let mut ptr = rs_treeseq.into_mut_ptr().unwrap(); + + // We allocated the tables via the Python allocator. + // Interally, tskit will free it with C's free, which is + // UB! + // To circumvent UB, we must manually do these steps: + unsafe { + tskit::bindings::tsk_table_collection_free(ptr.as_mut().tables); + pyo3::ffi::PyMem_Free(ptr.as_mut().tables as *mut std::ffi::c_void); + ptr.as_mut().tables = std::ptr::null_mut(); + } + let rv = unsafe { tskit::bindings::tsk_treeseq_free(ptr.as_mut()) }; assert_eq!(rv, 0); unsafe { From 64310013391a52941098378841fdc621bce6bf69 Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 13:57:52 -0700 Subject: [PATCH 17/18] fix comments --- python/zerocopy/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index e15155da..7e564621 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -147,7 +147,9 @@ fn test_treeseq_new_from_raw_tables_also_py_allocated() { // We allocated the tables via the Python allocator. // Interally, tskit will free it with C's free, which is // UB! - // To circumvent UB, we must manually do these steps: + // To circumvent UB, we must manually do the steps below. + // We know to do these steps b/c we have read the implementation + // of tsk_treeseq_free. unsafe { tskit::bindings::tsk_table_collection_free(ptr.as_mut().tables); pyo3::ffi::PyMem_Free(ptr.as_mut().tables as *mut std::ffi::c_void); From e5f82ab341da943eb59ba76ad29c322fda66609e Mon Sep 17 00:00:00 2001 From: Kevin Thornton Date: Tue, 2 Jun 2026 14:06:09 -0700 Subject: [PATCH 18/18] fix typo --- python/zerocopy/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/zerocopy/src/lib.rs b/python/zerocopy/src/lib.rs index 7e564621..661e0f8e 100644 --- a/python/zerocopy/src/lib.rs +++ b/python/zerocopy/src/lib.rs @@ -145,7 +145,7 @@ fn test_treeseq_new_from_raw_tables_also_py_allocated() { let mut ptr = rs_treeseq.into_mut_ptr().unwrap(); // We allocated the tables via the Python allocator. - // Interally, tskit will free it with C's free, which is + // Internally, tskit will free it with C's free, which is // UB! // To circumvent UB, we must manually do the steps below. // We know to do these steps b/c we have read the implementation