use std::{
marker::PhantomData,
os::raw::{c_char, c_void},
ptr::{self, NonNull},
sync::Arc,
};
mod errors;
#[cfg(any(test, feature = "test-hooks"))]
mod testing_hooks;
use errors::{
classify_column_view_metadata_error, classify_string_view_contract_error,
foreign_column_view_metadata_contract_error, foreign_string_view_contract_error,
};
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) use testing_hooks::{
adopt_owned_column_view_for_test, column_view_copy_fixed_width_to_host_for_benchmark,
column_view_to_fixed_width_host_bytes_for_test, column_view_to_raw_bytes_for_test,
trigger_fixed_width_host_byte_range_overflow_for_test,
};
use super::{convert::copy_view_to_owned, device_slice::DeviceSlice, owned::Column};
use crate::{
core::{
error::{
self, Error, ErrorCode, FACT_ACTUAL_COUNT, FACT_EXPECTED_COUNT, FACT_LEFT_TYPE_ID, FACT_RIGHT_TYPE_ID,
FACT_TYPE_ID, Result,
},
native_teardown::drop_device_owned_handle,
types::{CudfType, DataType, TypeId, materialize_bool},
},
ops::search,
runtime::execution_domain::ExecutionDomain,
};
fn fixed_width_host_byte_range_overflow_error(
operation: &'static str,
row_count: usize,
elem_size_bytes: usize,
type_id: TypeId,
) -> Error {
Error::new_with(ErrorCode::UnsupportedOperation, operation, "host byte range overflowed")
.with_fact(FACT_TYPE_ID, type_id)
.with_detail("resource", "host_byte_range")
.with_detail("reason", "size_overflowed_usize")
.with_detail(
"remedy",
"reduce row_count or choose a smaller fixed-width type so the active host byte range fits within usize",
)
.with_detail("row_count", row_count.to_string())
.with_detail("elem_size_bytes", elem_size_bytes.to_string())
}
pub struct ColumnView<'a> {
pub(crate) handle: NonNull<cudf_sys::cudf_column_view_handle_state>,
pub(crate) domain: Arc<ExecutionDomain>,
pub(crate) _lifetime: PhantomData<&'a ()>,
}
impl<'a> ColumnView<'a> {
pub(crate) unsafe fn from_raw_with_domain(
handle: cudf_sys::cudf_column_view_handle,
domain: Arc<ExecutionDomain>,
) -> Result<Self> {
Ok(Self {
handle: error::require_non_null(handle, "column_view::from_raw_with_domain", "column view")?,
domain,
_lifetime: PhantomData,
})
}
pub(crate) fn raw_handle(&self) -> cudf_sys::cudf_column_view_handle {
self.handle.as_ptr()
}
pub(crate) fn execution_domain(&self) -> &Arc<ExecutionDomain> {
&self.domain
}
pub(crate) fn slice_with_operation(&self, offset: i32, length: i32, operation: &'static str) -> Result<Self> {
let exec_options = self.domain.enter_execution()?;
self.slice_with_exec_options(offset, length, exec_options.options(), operation)
}
pub(crate) fn slice_with_exec_options(
&self,
offset: i32,
length: i32,
exec_options: &cudf_sys::cudf_exec_options_t,
operation: &'static str,
) -> Result<Self> {
let mut sliced = ptr::null_mut();
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_slice_ex(self.handle.as_ptr(), offset, length, exec_options, &mut sliced)
},
operation,
)?;
Ok(Self {
handle: error::require_non_null(sliced, operation, "sliced column view")?,
domain: Arc::clone(&self.domain),
_lifetime: PhantomData,
})
}
pub(crate) fn duplicate_with_operation(&self, operation: &'static str) -> Result<Self> {
let row_count = self.len()?;
let len = i32::try_from(row_count)
.map_err(|_| error::range_limit_error(operation, "row_count", "i32::MAX", row_count))?;
self.slice_with_operation(0, len, operation)
}
pub(crate) fn __debug_execution_domain_kind(&self) -> &'static str {
self.domain.debug_kind()
}
pub(crate) fn __debug_execution_domain_id(&self) -> u64 {
self.domain.debug_id()
}
pub(crate) fn __debug_execution_domain_stream_value(&self) -> usize {
self.domain.debug_stream_value()
}
pub(crate) fn __debug_execution_domain_mr_value(&self) -> usize {
self.domain.debug_mr_value()
}
fn synchronize_host_visibility(&self, operation: &'static str) -> Result<()> {
self.domain.synchronize_for_host_visibility(operation)
}
pub fn data_type(&self) -> Result<DataType> {
let mut raw_id: cudf_sys::cudf_type_id_t = 0;
let mut scale = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_data_type(self.handle.as_ptr(), &mut raw_id, &mut scale) },
"column_view::data_type",
)
.map_err(classify_column_view_metadata_error)?;
DataType::from_raw_parts(raw_id as i32, scale)
}
pub fn len(&self) -> Result<usize> {
let mut out: i32 = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_size(self.handle.as_ptr(), &mut out) },
"column_view::len",
)
.map_err(classify_column_view_metadata_error)?;
usize::try_from(out).map_err(|_| {
foreign_column_view_metadata_contract_error("column_view::len", "FFI returned negative column size")
.with_detail("raw_size", out.to_string())
})
}
pub fn is_empty(&self) -> Result<bool> {
Ok(self.len()? == 0)
}
pub fn null_count(&self) -> Result<usize> {
let mut out: i32 = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_null_count(self.handle.as_ptr(), &mut out) },
"column_view::null_count",
)
.map_err(classify_column_view_metadata_error)?;
if out < 0 {
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_null_count_ex(self.handle.as_ptr(), exec_options.options(), &mut out)
},
"column_view::null_count",
)
.map_err(classify_column_view_metadata_error)?;
}
usize::try_from(out).map_err(|_| {
foreign_column_view_metadata_contract_error("column_view::null_count", "column null count must be >= 0")
.with_detail("null_count", out.to_string())
})
}
pub fn nullable(&self) -> Result<bool> {
let mut out: i32 = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_nullable(self.handle.as_ptr(), &mut out) },
"column_view::nullable",
)
.map_err(classify_column_view_metadata_error)?;
Ok(out != 0)
}
pub fn num_children(&self) -> Result<usize> {
let mut out: i32 = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_num_children(self.handle.as_ptr(), &mut out) },
"column_view::num_children",
)
.map_err(classify_column_view_metadata_error)?;
usize::try_from(out).map_err(|_| {
foreign_column_view_metadata_contract_error("column_view::num_children", "column child count must be >= 0")
.with_detail("num_children", out.to_string())
})
}
pub fn child(&self, index: usize) -> Result<ColumnView<'a>> {
let num_children = self.num_children()?;
if index >= num_children {
return Err(Error::new_with(
ErrorCode::IndexOutOfBounds,
"column_view::child",
"column child index out of bounds",
)
.with_detail("child_index", index.to_string())
.with_detail("num_children", num_children.to_string()));
}
let index = i32::try_from(index)
.map_err(|_| error::range_limit_error("column_view::child", "child_index", "i32::MAX", index))?;
let mut child_handle = ptr::null_mut();
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_child(self.handle.as_ptr(), index, &mut child_handle) },
"column_view::child",
)?;
Ok(ColumnView {
handle: error::require_non_null(child_handle, "column_view::child", "column child view")?,
domain: Arc::clone(&self.domain),
_lifetime: PhantomData,
})
}
pub(crate) fn list_sliced_child(&self) -> Result<ColumnView<'a>> {
let mut child_handle = ptr::null_mut();
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_list_sliced_child_ex(
self.handle.as_ptr(),
exec_options.options(),
&mut child_handle,
)
},
"column_view::list_sliced_child",
)?;
Ok(ColumnView {
handle: error::require_non_null(child_handle, "column_view::list_sliced_child", "list child view")?,
domain: Arc::clone(&self.domain),
_lifetime: PhantomData,
})
}
pub fn device_data_ptr(&self) -> Result<*const c_void> {
let mut out: *const c_void = std::ptr::null();
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_data_ptr(self.handle.as_ptr(), &mut out) },
"column_view::device_data_ptr",
)?;
Ok(out)
}
pub(crate) fn string_chars_ptr_and_size(&self) -> Result<(*const c_void, usize)> {
let mut out_ptr: *const c_void = std::ptr::null();
let mut out_size: usize = 0;
let exec_options = self.domain.enter_execution()?;
let status = unsafe {
cudf_sys::cudf_column_view_string_chars_ptr_and_size_ex(
self.handle.as_ptr(),
exec_options.options(),
&mut out_ptr,
&mut out_size,
)
};
error::check_status_in(status, "column_view::string_chars_ptr_and_size")
.map_err(classify_string_view_contract_error)?;
Ok((out_ptr, out_size))
}
fn validate_string_view_layout_for_host_materialization(&self, operation: &'static str) -> Result<()> {
let num_children =
self.num_children().map_err(|e| classify_string_view_contract_error(e).reanchor(operation))?;
if num_children >= 1 {
return Ok(());
}
Err(foreign_string_view_contract_error(
"column_view::string_chars_ptr_and_size",
"STRING column view must expose offsets and chars children",
)
.with_fact(FACT_EXPECTED_COUNT, 1usize)
.with_fact(FACT_ACTUAL_COUNT, num_children)
.reanchor(operation))
}
pub(crate) fn null_mask_ptr(&self) -> Result<Option<*const c_void>> {
let mut out: *const c_void = std::ptr::null();
let status = unsafe { cudf_sys::cudf_column_view_null_mask_ptr(self.handle.as_ptr(), &mut out) };
if status == cudf_sys::CUDF_ERROR_NULL {
return Ok(None);
}
error::check_status_in(status, "column_view::null_mask_ptr")?;
Ok(Some(out))
}
pub fn offset(&self) -> Result<i32> {
let mut out: i32 = 0;
error::check_status_in(
unsafe { cudf_sys::cudf_column_view_offset(self.handle.as_ptr(), &mut out) },
"column_view::offset",
)
.map_err(classify_column_view_metadata_error)?;
Ok(out)
}
pub fn to_vec<T: CudfType>(&self) -> Result<Vec<T>> {
let dt = self.data_type()?;
if dt.id() != T::TYPE_ID {
return Err(Error::new_with(
ErrorCode::TypeMismatch,
"column_view::to_vec",
"column type does not match requested type",
)
.with_fact(FACT_LEFT_TYPE_ID, dt.id())
.with_fact(FACT_RIGHT_TYPE_ID, T::TYPE_ID));
}
if T::TYPE_ID == TypeId::Bool8 {
return self.to_raw_bytes()?.into_iter().map(|raw| materialize_bool(raw, "column_view::to_vec")).collect();
}
let size = self.len()?;
let mut buf = vec![unsafe { std::mem::zeroed::<T>() }; size];
let buf_size = size * std::mem::size_of::<T>();
self.synchronize_host_visibility("column_view::to_vec")?;
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_to_host_ex(
self.handle.as_ptr(),
buf.as_mut_ptr() as *mut c_void,
buf_size,
exec_options.options(),
)
},
"column_view::to_vec",
)?;
Ok(buf)
}
pub fn to_optional_vec<T: CudfType>(&self) -> Result<Vec<Option<T>>> {
if T::TYPE_ID == TypeId::Bool8 {
return self.to_optional_vec_bool::<T>();
}
let vals = self.to_vec::<T>()?;
match self.to_host_validity()? {
None => Ok(vals.into_iter().map(Some).collect()),
Some(mask) => Ok(vals.into_iter().zip(mask).map(|(v, valid)| if valid { Some(v) } else { None }).collect()),
}
}
pub(crate) fn to_raw_bytes(&self) -> Result<Vec<u8>> {
let dt = self.data_type()?;
let is_one_byte = matches!(dt.id(), TypeId::Bool8 | TypeId::Int8 | TypeId::UInt8);
if !is_one_byte {
return Err(Error::new_with(
ErrorCode::UnsupportedDtype,
"column_view::to_raw_bytes",
"to_raw_bytes requires a 1-byte type",
)
.with_fact(FACT_TYPE_ID, dt.id()));
}
let size = self.len()?;
let mut buf = vec![0u8; size];
self.synchronize_host_visibility("column_view::to_raw_bytes")?;
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_to_host_ex(
self.handle.as_ptr(),
buf.as_mut_ptr() as *mut c_void,
size,
exec_options.options(),
)
},
"column_view::to_raw_bytes",
)?;
Ok(buf)
}
fn to_optional_vec_bool<T: CudfType>(&self) -> Result<Vec<Option<T>>> {
let raw = self.to_raw_bytes()?;
match self.to_host_validity()? {
None => {
raw.into_iter().map(|byte| materialize_bool(byte, "column_view::to_optional_vec").map(Some)).collect()
},
Some(mask) => {
raw.into_iter()
.zip(mask)
.map(|(byte, valid)| {
if valid { materialize_bool(byte, "column_view::to_optional_vec").map(Some) } else { Ok(None) }
})
.collect()
},
}
}
pub(crate) fn to_fixed_width_host_bytes(&self) -> Result<Vec<u8>> {
let dt = self.data_type()?;
if self.offset()? != 0 {
let normalized = super::convert::copy_view_to_owned(self)?;
return normalized.view()?.to_fixed_width_host_bytes();
}
let elem_size = usize::try_from(dt.id().size_in_bytes()?.ok_or_else(|| {
Error::new_with(
ErrorCode::UnsupportedDtype,
"column_view::to_fixed_width_host_bytes",
"to_fixed_width_host_bytes requires a fixed-width type",
)
.with_fact(FACT_TYPE_ID, dt.id())
})?)
.map_err(|_| {
Error::new_with(
ErrorCode::ForeignInvariantViolation,
"column_view::to_fixed_width_host_bytes",
"fixed-width element size must be >= 0",
)
.with_fact(FACT_TYPE_ID, dt.id())
})?;
let size = self.len()?;
let buf_size = size.checked_mul(elem_size).ok_or_else(|| {
fixed_width_host_byte_range_overflow_error(
"column_view::to_fixed_width_host_bytes",
size,
elem_size,
dt.id(),
)
})?;
let mut buf = vec![0u8; buf_size];
self.synchronize_host_visibility("column_view::to_fixed_width_host_bytes")?;
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_to_host_ex(
self.handle.as_ptr(),
buf.as_mut_ptr() as *mut c_void,
buf_size,
exec_options.options(),
)
},
"column_view::to_fixed_width_host_bytes",
)?;
Ok(buf)
}
fn to_host_null_mask_bytes_with_operation(&self, operation: &'static str) -> Result<Option<Vec<u8>>> {
if !self.nullable().map_err(|e| e.reanchor(operation))? {
return Ok(None);
}
let size = self.len().map_err(|e| e.reanchor(operation))?;
let mask_bytes = size.div_ceil(8);
let alloc_bytes = mask_bytes.div_ceil(64) * 64;
let mut mask = vec![0u8; alloc_bytes];
self.synchronize_host_visibility(operation)?;
let exec_options = self.domain.enter_execution()?;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_null_mask_to_host_ex(
self.handle.as_ptr(),
mask.as_mut_ptr(),
alloc_bytes,
exec_options.options(),
)
},
operation,
)?;
mask.truncate(mask_bytes);
Ok(Some(mask))
}
pub fn to_host_null_mask_bytes(&self) -> Result<Option<Vec<u8>>> {
self.to_host_null_mask_bytes_with_operation("column_view::to_host_null_mask_bytes")
}
pub fn to_host_validity(&self) -> Result<Option<Vec<bool>>> {
let Some(mask) = self.to_host_null_mask_bytes_with_operation("column_view::to_host_validity")? else {
return Ok(None);
};
let size = self.len().map_err(|e| e.reanchor("column_view::to_host_validity"))?;
let validity: Vec<bool> = (0..size).map(|i| (mask[i / 8] >> (i % 8)) & 1 != 0).collect();
Ok(Some(validity))
}
pub fn to_strings_lossy(&self) -> Result<Vec<String>> {
let strings = self.to_strings_host_raw("column_view::to_strings_lossy")?;
match self.to_host_validity().map_err(|e| e.reanchor("column_view::to_strings_lossy"))? {
None => Ok(strings),
Some(valid) => Ok(strings
.into_iter()
.zip(valid)
.map(|(value, is_valid)| if is_valid { value } else { String::new() })
.collect()),
}
}
fn to_strings_host_raw(&self, operation: &'static str) -> Result<Vec<String>> {
let dt = self.data_type()?;
if dt.id() != TypeId::String {
return Err(Error::new_with(ErrorCode::TypeMismatch, operation, "column type is not STRING")
.with_fact(FACT_LEFT_TYPE_ID, dt.id())
.with_fact(FACT_RIGHT_TYPE_ID, TypeId::String));
}
if self.offset()? != 0 {
let normalized = copy_view_to_owned(self)?;
return normalized.view()?.to_strings_host_raw(operation);
}
let size = self.len()?;
if size == 0 {
return Ok(Vec::new());
}
self.validate_string_view_layout_for_host_materialization(operation)?;
let offsets_count = size + 1;
let mut offsets = vec![0i32; offsets_count];
let offsets_buf_size = std::mem::size_of_val(offsets.as_slice());
let (_, chars_size) = self.string_chars_ptr_and_size().map_err(|e| e.reanchor(operation))?;
let expected_total_chars_bytes = i32::try_from(chars_size).map_err(|_| {
foreign_string_view_contract_error(operation, "string chars span exceeds i32::MAX")
.with_detail("chars_size", chars_size.to_string())
})?;
let mut chars = vec![0u8; chars_size];
let exec_options = self.domain.enter_execution()?;
let chars_buf = if chars.is_empty() { ptr::null_mut() } else { chars.as_mut_ptr() as *mut c_char };
let mut total_chars_bytes = 0;
error::check_status_in(
unsafe {
cudf_sys::cudf_column_view_strings_to_host_ex(
self.handle.as_ptr(),
offsets.as_mut_ptr(),
offsets_buf_size,
chars_buf,
chars.len(),
&mut total_chars_bytes,
exec_options.options(),
)
},
operation,
)
.map_err(classify_string_view_contract_error)?;
if total_chars_bytes != expected_total_chars_bytes {
return Err(foreign_string_view_contract_error(
operation,
"string chars size changed during host materialization",
)
.with_detail("expected_total_chars_bytes", expected_total_chars_bytes.to_string())
.with_detail("actual_total_chars_bytes", total_chars_bytes.to_string()));
}
let mut result = Vec::with_capacity(size);
for i in 0..size {
let start = usize::try_from(offsets[i]).map_err(|_| {
foreign_string_view_contract_error(operation, "string offset is negative")
.with_detail("row", i.to_string())
.with_detail("offset_value", offsets[i].to_string())
})?;
let end = usize::try_from(offsets[i + 1]).map_err(|_| {
foreign_string_view_contract_error(operation, "string offset is negative")
.with_detail("row", (i + 1).to_string())
.with_detail("offset_value", offsets[i + 1].to_string())
})?;
let s = std::str::from_utf8(&chars[start..end]).map_err(|e| {
foreign_string_view_contract_error(
operation,
format!("invalid UTF-8 in string column at row {}: {}", i, e),
)
.with_detail("row", i.to_string())
})?;
result.push(s.to_string());
}
Ok(result)
}
pub fn to_optional_strings(&self) -> Result<Vec<Option<String>>> {
let strings = self.to_strings_host_raw("column_view::to_optional_strings")?;
match self.to_host_validity().map_err(|e| e.reanchor("column_view::to_optional_strings"))? {
None => Ok(strings.into_iter().map(Some).collect()),
Some(valid) => Ok(strings.into_iter().zip(valid).map(|(s, v)| if v { Some(s) } else { None }).collect()),
}
}
pub fn device_slice<T: CudfType>(&self) -> Result<DeviceSlice<'_>> {
let dt = self.data_type()?;
if dt.id() != T::TYPE_ID {
return Err(Error::new_with(
ErrorCode::TypeMismatch,
"column_view::device_slice",
"column type does not match requested type",
)
.with_fact(FACT_LEFT_TYPE_ID, dt.id())
.with_fact(FACT_RIGHT_TYPE_ID, T::TYPE_ID));
}
let base_ptr = self.device_data_ptr()?;
let offset = self.offset()? as usize;
let len = self.len()?;
let elem_size = std::mem::size_of::<T>();
let ptr = if base_ptr.is_null() {
base_ptr as *mut c_void
} else {
unsafe { (base_ptr as *const u8).add(offset * elem_size) as *mut c_void }
};
Ok(DeviceSlice { ptr, len, type_id: T::TYPE_ID, elem_size, _lifetime: PhantomData })
}
}
impl ColumnView<'_> {
pub fn isin(&self, values: &ColumnView<'_>) -> Result<Column> {
let input_type_id = match self.data_type() {
Ok(dt) => dt.id(),
Err(e) => return Err(e.reanchor("column_view::isin")),
};
let input_type_id_str = format!("{:?}", input_type_id).to_lowercase();
let values_type_id = values
.data_type()
.map_err(|e| e.reanchor("column_view::isin").with_detail("input_type_id", input_type_id_str.clone()))?;
let values_type_id_str = format!("{:?}", values_type_id.id()).to_lowercase();
if input_type_id != values_type_id.id() {
return Err(Error::new_with(
ErrorCode::TypeMismatch,
"column_view::isin",
"isin requires both inputs to have the same dtype",
)
.with_fact(FACT_LEFT_TYPE_ID, input_type_id)
.with_fact(FACT_RIGHT_TYPE_ID, values_type_id.id()));
}
ExecutionDomain::require_same_device_for_mixed_inputs(
self.execution_domain(),
values.execution_domain(),
"column_view::isin",
)
.map_err(|e| {
e.with_detail("input_type_id", input_type_id_str.clone())
.with_detail("values_type_id", values_type_id_str.clone())
})?;
let _consumer_fence =
ExecutionDomain::insert_stream_dependency(values.execution_domain(), &self.domain, "column_view::isin")
.map_err(|e| {
e.with_detail("input_type_id", input_type_id_str.clone())
.with_detail("values_type_id", values_type_id_str.clone())
})?;
let exec_options = self.domain.enter_execution()?;
search::contains_impl(values, self, &self.domain, exec_options.options(), "column_view::isin").map_err(|e| {
e.reanchor("column_view::isin")
.with_detail("input_type_id", input_type_id_str)
.with_detail("values_type_id", values_type_id_str)
})
}
}
impl Drop for ColumnView<'_> {
fn drop(&mut self) {
drop_device_owned_handle(self.handle.as_ptr(), self.domain.device_id(), "ColumnView::drop", |owned| unsafe {
cudf_sys::cudf_column_view_destroy(owned)
});
}
}