Replace FreeTypeFaceHelpers with a safe wrapper struct (#38634)

Testing: only safety annotations change, no tests are required.
Fixes: https://github.com/servo/servo/issues/38627

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-08-17 23:46:16 +02:00 committed by GitHub
parent 4de9a9d100
commit e19fade481
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 212 additions and 167 deletions

View file

@ -4,17 +4,13 @@
use std::ffi::CString;
use std::fs::File;
use std::os::raw::c_long;
use std::{mem, ptr};
use app_units::Au;
use euclid::default::{Point2D, Rect, Size2D};
use freetype_sys::{
FT_Done_Face, FT_F26Dot6, FT_FACE_FLAG_COLOR, FT_FACE_FLAG_FIXED_SIZES, FT_FACE_FLAG_SCALABLE,
FT_Face, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_Int32, FT_KERNING_DEFAULT,
FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Long, FT_New_Face,
FT_New_Memory_Face, FT_Pos, FT_Select_Size, FT_Set_Char_Size, FT_Size_Metrics, FT_SizeRec,
FT_UInt, FT_ULong, FT_Vector,
FT_F26Dot6, FT_Get_Char_Index, FT_Get_Kerning, FT_GlyphSlot, FT_KERNING_DEFAULT,
FT_LOAD_DEFAULT, FT_LOAD_NO_HINTING, FT_Load_Glyph, FT_Size_Metrics, FT_SizeRec, FT_UInt,
FT_ULong, FT_Vector,
};
use log::debug;
use memmap2::Mmap;
@ -37,13 +33,9 @@ use crate::font::{
};
use crate::font_template::FontTemplateDescriptor;
use crate::glyph::GlyphId;
use crate::platform::freetype::freetype_face::FreeTypeFace;
use crate::system_font_service::FontIdentifier;
// This constant is not present in the freetype
// bindings due to bindgen not handling the way
// the macro is defined.
const FT_LOAD_TARGET_LIGHT: FT_UInt = 1 << 16;
/// Convert FreeType-style 26.6 fixed point to an [`f64`].
fn fixed_26_dot_6_to_float(fixed: FT_F26Dot6) -> f64 {
fixed as f64 / 64.0
@ -68,7 +60,7 @@ impl FontTableMethods for FontTable {
#[derive(Debug)]
#[allow(unused)]
pub struct PlatformFont {
face: ReentrantMutex<FT_Face>,
face: ReentrantMutex<FreeTypeFace>,
requested_face_size: Au,
actual_face_size: Au,
@ -76,28 +68,6 @@ pub struct PlatformFont {
table_provider_data: FreeTypeFaceTableProviderData,
}
// FT_Face can be used in multiple threads, but from only one thread at a time.
// It's protected with a ReentrantMutex for PlatformFont.
// See https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face.
unsafe impl Sync for PlatformFont {}
unsafe impl Send for PlatformFont {}
impl Drop for PlatformFont {
fn drop(&mut self) {
let face = self.face.lock();
assert!(!face.is_null());
unsafe {
// The FreeType documentation says that both `FT_New_Face` and `FT_Done_Face`
// should be protected by a mutex.
// See https://freetype.org/freetype2/docs/reference/ft2-library_setup.html.
let _guard = FreeTypeLibraryHandle::get().lock();
if FT_Done_Face(*face) != 0 {
panic!("FT_Done_Face failed");
}
}
}
}
impl PlatformFontMethods for PlatformFont {
fn new_from_data(
_font_identifier: FontIdentifier,
@ -106,20 +76,7 @@ impl PlatformFontMethods for PlatformFont {
) -> Result<PlatformFont, &'static str> {
let library = FreeTypeLibraryHandle::get().lock();
let data: &[u8] = font_data.as_ref();
let mut face: FT_Face = ptr::null_mut();
let result = unsafe {
FT_New_Memory_Face(
library.freetype_library,
data.as_ptr(),
data.len() as FT_Long,
0, /* face_index */
&mut face,
)
};
if 0 != result || face.is_null() {
return Err("Could not create FreeType face");
}
let face = FreeTypeFace::new_from_memory(&library, data)?;
let (requested_face_size, actual_face_size) = match requested_size {
Some(requested_size) => (requested_size, face.set_size(requested_size)?),
@ -138,22 +95,10 @@ impl PlatformFontMethods for PlatformFont {
font_identifier: LocalFontIdentifier,
requested_size: Option<Au>,
) -> Result<PlatformFont, &'static str> {
let mut face: FT_Face = ptr::null_mut();
let library = FreeTypeLibraryHandle::get().lock();
let filename = CString::new(&*font_identifier.path).expect("filename contains NUL byte!");
let result = unsafe {
FT_New_Face(
library.freetype_library,
filename.as_ptr(),
font_identifier.index() as FT_Long,
&mut face,
)
};
if 0 != result || face.is_null() {
return Err("Could not create FreeType face");
}
let face = FreeTypeFace::new_from_file(&library, &filename, font_identifier.index())?;
let (requested_face_size, actual_face_size) = match requested_size {
Some(requested_size) => (requested_size, face.set_size(requested_size)?),
@ -210,10 +155,9 @@ impl PlatformFontMethods for PlatformFont {
fn glyph_index(&self, codepoint: char) -> Option<GlyphId> {
let face = self.face.lock();
assert!(!face.is_null());
unsafe {
let idx = FT_Get_Char_Index(*face, codepoint as FT_ULong);
let idx = FT_Get_Char_Index(face.as_ptr(), codepoint as FT_ULong);
if idx != 0 as FT_UInt {
Some(idx as GlyphId)
} else {
@ -228,12 +172,11 @@ impl PlatformFontMethods for PlatformFont {
fn glyph_h_kerning(&self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel {
let face = self.face.lock();
assert!(!face.is_null());
let mut delta = FT_Vector { x: 0, y: 0 };
unsafe {
FT_Get_Kerning(
*face,
face.as_ptr(),
first_glyph,
second_glyph,
FT_KERNING_DEFAULT,
@ -245,16 +188,15 @@ impl PlatformFontMethods for PlatformFont {
fn glyph_h_advance(&self, glyph: GlyphId) -> Option<FractionalPixel> {
let face = self.face.lock();
assert!(!face.is_null());
let load_flags = face.glyph_load_flags();
let result = unsafe { FT_Load_Glyph(*face, glyph as FT_UInt, load_flags) };
let result = unsafe { FT_Load_Glyph(face.as_ptr(), glyph as FT_UInt, load_flags) };
if 0 != result {
debug!("Unable to load glyph {}. reason: {:?}", glyph, result);
return None;
}
let void_glyph = unsafe { (**face).glyph };
let void_glyph = face.as_ref().glyph;
let slot: FT_GlyphSlot = void_glyph;
assert!(!slot.is_null());
@ -263,12 +205,11 @@ impl PlatformFontMethods for PlatformFont {
}
fn metrics(&self) -> FontMetrics {
let face_ptr = *self.face.lock();
let face = unsafe { &*face_ptr };
let face = self.face.lock();
let font_ref = self.table_provider_data.font_ref();
// face.size is a *c_void in the bindings, presumably to avoid recursive structural types
let freetype_size: &FT_SizeRec = unsafe { mem::transmute(&(*face.size)) };
let freetype_size: &FT_SizeRec = unsafe { &*face.as_ref().size };
let freetype_metrics: &FT_Size_Metrics = &(freetype_size).metrics;
let mut max_advance;
@ -277,7 +218,7 @@ impl PlatformFontMethods for PlatformFont {
let mut line_height;
let mut y_scale = 0.0;
let mut em_height;
if face_ptr.scalable() {
if face.scalable() {
// Prefer FT_Size_Metrics::y_scale to y_ppem as y_ppem does not have subpixel accuracy.
//
// FT_Size_Metrics::y_scale is in 16.16 fixed point format. Its (fractional) value is a
@ -287,11 +228,11 @@ impl PlatformFontMethods for PlatformFont {
// This converts the value to a float without losing precision.
y_scale = freetype_metrics.y_scale as f64 / 65535.0 / 64.0;
max_advance = (face.max_advance_width as f64) * y_scale;
max_ascent = (face.ascender as f64) * y_scale;
max_descent = -(face.descender as f64) * y_scale;
line_height = (face.height as f64) * y_scale;
em_height = (face.units_per_EM as f64) * y_scale;
max_advance = (face.as_ref().max_advance_width as f64) * y_scale;
max_ascent = (face.as_ref().ascender as f64) * y_scale;
max_descent = -(face.as_ref().descender as f64) * y_scale;
line_height = (face.as_ref().height as f64) * y_scale;
em_height = (face.as_ref().units_per_EM as f64) * y_scale;
} else {
max_advance = fixed_26_dot_6_to_float(freetype_metrics.max_advance);
max_ascent = fixed_26_dot_6_to_float(freetype_metrics.ascender);
@ -307,7 +248,7 @@ impl PlatformFontMethods for PlatformFont {
// Bug 1267909 - Even if the font is not explicitly scalable, if the face has color
// bitmaps, it should be treated as scalable and scaled to the desired size. Metrics
// based on y_ppem need to be rescaled for the adjusted size.
if face_ptr.color() {
if face.color() {
em_height = self.requested_face_size.to_f64_px();
let adjust_scale = em_height / (freetype_metrics.y_ppem as f64);
max_advance *= adjust_scale;
@ -327,8 +268,8 @@ impl PlatformFontMethods for PlatformFont {
// Convert using a formula similar to what CTFont returns for consistency.
let leading = line_height - (max_ascent + max_descent);
let underline_size = face.underline_thickness as f64 * y_scale;
let underline_offset = face.underline_position as f64 * y_scale + 0.5;
let underline_size = face.as_ref().underline_thickness as f64 * y_scale;
let underline_offset = face.as_ref().underline_position as f64 * y_scale + 0.5;
// The default values for strikeout size and offset. Use OpenType spec's suggested position
// for Roman font as the default for offset.
@ -411,16 +352,15 @@ impl PlatformFontMethods for PlatformFont {
fn typographic_bounds(&self, glyph_id: GlyphId) -> Rect<f32> {
let face = self.face.lock();
assert!(!face.is_null());
let load_flags = FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING;
let result = unsafe { FT_Load_Glyph(*face, glyph_id as FT_UInt, load_flags) };
let result = unsafe { FT_Load_Glyph(face.as_ptr(), glyph_id as FT_UInt, load_flags) };
if 0 != result {
debug!("Unable to load glyph {}. reason: {:?}", glyph_id, result);
return Rect::default();
}
let metrics = unsafe { &(*(**face).glyph).metrics };
let metrics = unsafe { &(*face.as_ref().glyph).metrics };
Rect::new(
Point2D::new(
@ -448,85 +388,6 @@ impl PlatformFont {
}
}
trait FreeTypeFaceHelpers {
fn scalable(self) -> bool;
fn color(self) -> bool;
fn set_size(self, pt_size: Au) -> Result<Au, &'static str>;
fn glyph_load_flags(self) -> FT_Int32;
}
impl FreeTypeFaceHelpers for FT_Face {
fn scalable(self) -> bool {
unsafe { (*self).face_flags & FT_FACE_FLAG_SCALABLE as c_long != 0 }
}
fn color(self) -> bool {
unsafe { (*self).face_flags & FT_FACE_FLAG_COLOR as c_long != 0 }
}
fn set_size(self, requested_size: Au) -> Result<Au, &'static str> {
if self.scalable() {
let size_in_fixed_point = (requested_size.to_f64_px() * 64.0 + 0.5) as FT_F26Dot6;
let result = unsafe { FT_Set_Char_Size(self, size_in_fixed_point, 0, 72, 72) };
if 0 != result {
return Err("FT_Set_Char_Size failed");
}
return Ok(requested_size);
}
let requested_size = (requested_size.to_f64_px() * 64.0) as FT_Pos;
let get_size_at_index = |index| unsafe {
(
(*(*self).available_sizes.offset(index as isize)).x_ppem,
(*(*self).available_sizes.offset(index as isize)).y_ppem,
)
};
let mut best_index = 0;
let mut best_size = get_size_at_index(0);
let mut best_dist = best_size.1 - requested_size;
for strike_index in 1..unsafe { (*self).num_fixed_sizes } {
let new_scale = get_size_at_index(strike_index);
let new_distance = new_scale.1 - requested_size;
// Distance is positive if strike is larger than desired size,
// or negative if smaller. If previously a found smaller strike,
// then prefer a larger strike. Otherwise, minimize distance.
if (best_dist < 0 && new_distance >= best_dist) || new_distance.abs() <= best_dist {
best_dist = new_distance;
best_size = new_scale;
best_index = strike_index;
}
}
if 0 == unsafe { FT_Select_Size(self, best_index) } {
Ok(Au::from_f64_px(best_size.1 as f64 / 64.0))
} else {
Err("FT_Select_Size failed")
}
}
fn glyph_load_flags(self) -> FT_Int32 {
let mut load_flags = FT_LOAD_DEFAULT;
// Default to slight hinting, which is what most
// Linux distros use by default, and is a better
// default than no hinting.
// TODO(gw): Make this configurable.
load_flags |= FT_LOAD_TARGET_LIGHT as i32;
let face_flags = unsafe { (*self).face_flags };
if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 {
// We only set FT_LOAD_COLOR if there are bitmap strikes; COLR (color-layer) fonts
// will be handled internally in Servo. In that case WebRender will just be asked to
// paint individual layers.
load_flags |= FT_LOAD_COLOR;
}
load_flags as FT_Int32
}
}
#[derive(Clone)]
enum FreeTypeFaceTableProviderData {
Web(FontData),

View file

@ -0,0 +1,183 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::ffi::{CStr, c_long};
use std::ptr;
use app_units::Au;
use freetype_sys::{
FT_Done_Face, FT_F26Dot6, FT_FACE_FLAG_COLOR, FT_FACE_FLAG_FIXED_SIZES, FT_FACE_FLAG_SCALABLE,
FT_Face, FT_FaceRec, FT_Int32, FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_Long, FT_New_Face,
FT_New_Memory_Face, FT_Pos, FT_Select_Size, FT_Set_Char_Size, FT_UInt,
};
use crate::platform::freetype::library_handle::FreeTypeLibraryHandle;
// This constant is not present in the freetype
// bindings due to bindgen not handling the way
// the macro is defined.
const FT_LOAD_TARGET_LIGHT: FT_UInt = 1 << 16;
/// A safe wrapper around [FT_Face].
#[derive(Debug)]
pub(crate) struct FreeTypeFace {
/// ## Safety Invariant
/// The pointer must have been returned from [FT_New_Face] or [FT_New_Memory_Face]
/// and must not be freed before `FreetypeFace::drop` is called.
face: ptr::NonNull<FT_FaceRec>,
}
impl FreeTypeFace {
pub(crate) fn new_from_memory(
library: &FreeTypeLibraryHandle,
data: &[u8],
) -> Result<Self, &'static str> {
let mut face = ptr::null_mut();
let result = unsafe {
FT_New_Memory_Face(
library.freetype_library,
data.as_ptr(),
data.len() as FT_Long,
0,
&mut face,
)
};
if 0 != result {
return Err("Could not create FreeType face");
}
let Some(face) = ptr::NonNull::new(face) else {
return Err("Could not create FreeType face");
};
Ok(Self { face })
}
pub(crate) fn new_from_file(
library: &FreeTypeLibraryHandle,
filename: &CStr,
index: u32,
) -> Result<Self, &'static str> {
let mut face = ptr::null_mut();
let result = unsafe {
FT_New_Face(
library.freetype_library,
filename.as_ptr(),
index as FT_Long,
&mut face,
)
};
if 0 != result {
return Err("Could not create FreeType face");
}
let Some(face) = ptr::NonNull::new(face) else {
return Err("Could not create FreeType face");
};
Ok(Self { face })
}
pub(crate) fn as_ref(&self) -> &FT_FaceRec {
unsafe { self.face.as_ref() }
}
pub(crate) fn as_ptr(&self) -> FT_Face {
self.face.as_ptr()
}
/// Return true iff the font face flags contain [FT_FACE_FLAG_SCALABLE].
pub(crate) fn scalable(&self) -> bool {
self.as_ref().face_flags & FT_FACE_FLAG_SCALABLE as c_long != 0
}
/// Return true iff the font face flags contain [FT_FACE_FLAG_COLOR].
pub(crate) fn color(&self) -> bool {
self.as_ref().face_flags & FT_FACE_FLAG_COLOR as c_long != 0
}
/// Scale the font to the given size if it is scalable, or select the closest
/// available size if it is not, preferring larger sizes over smaller ones.
///
/// Returns the selected size on success and a error message on failure
pub(crate) fn set_size(&self, requested_size: Au) -> Result<Au, &'static str> {
if self.scalable() {
let size_in_fixed_point = (requested_size.to_f64_px() * 64.0 + 0.5) as FT_F26Dot6;
let result =
unsafe { FT_Set_Char_Size(self.face.as_ptr(), size_in_fixed_point, 0, 72, 72) };
if 0 != result {
return Err("FT_Set_Char_Size failed");
}
return Ok(requested_size);
}
let requested_size = (requested_size.to_f64_px() * 64.0) as FT_Pos;
let get_size_at_index = |index| unsafe {
(
(*self.as_ref().available_sizes.offset(index as isize)).x_ppem,
(*self.as_ref().available_sizes.offset(index as isize)).y_ppem,
)
};
let mut best_index = 0;
let mut best_size = get_size_at_index(0);
let mut best_dist = best_size.1 - requested_size;
for strike_index in 1..self.as_ref().num_fixed_sizes {
let new_scale = get_size_at_index(strike_index);
let new_distance = new_scale.1 - requested_size;
// Distance is positive if strike is larger than desired size,
// or negative if smaller. If previously a found smaller strike,
// then prefer a larger strike. Otherwise, minimize distance.
if (best_dist < 0 && new_distance >= best_dist) || new_distance.abs() <= best_dist {
best_dist = new_distance;
best_size = new_scale;
best_index = strike_index;
}
}
if 0 == unsafe { FT_Select_Size(self.face.as_ptr(), best_index) } {
Ok(Au::from_f64_px(best_size.1 as f64 / 64.0))
} else {
Err("FT_Select_Size failed")
}
}
/// Select a reasonable set of glyph loading flags for the font.
pub(crate) fn glyph_load_flags(&self) -> FT_Int32 {
let mut load_flags = FT_LOAD_DEFAULT;
// Default to slight hinting, which is what most
// Linux distros use by default, and is a better
// default than no hinting.
// TODO(gw): Make this configurable.
load_flags |= FT_LOAD_TARGET_LIGHT as i32;
let face_flags = self.as_ref().face_flags;
if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 {
// We only set FT_LOAD_COLOR if there are bitmap strikes; COLR (color-layer) fonts
// will be handled internally in Servo. In that case WebRender will just be asked to
// paint individual layers.
load_flags |= FT_LOAD_COLOR;
}
load_flags as FT_Int32
}
}
/// FT_Face can be used in multiple threads, but from only one thread at a time.
/// See <https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face>.
unsafe impl Send for FreeTypeFace {}
impl Drop for FreeTypeFace {
fn drop(&mut self) {
// The FreeType documentation says that both `FT_New_Face` and `FT_Done_Face`
// should be protected by a mutex.
// See https://freetype.org/freetype2/docs/reference/ft2-library_setup.html.
let _guard = FreeTypeLibraryHandle::get().lock();
if unsafe { FT_Done_Face(self.face.as_ptr()) } != 0 {
panic!("FT_Done_Face failed");
}
}
}

View file

@ -11,12 +11,12 @@ use freetype_sys::{
FT_Add_Default_Modules, FT_Done_Library, FT_Library, FT_Memory, FT_MemoryRec, FT_New_Library,
};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use parking_lot::Mutex;
use parking_lot::ReentrantMutex;
use servo_allocator::libc_compat::{free, malloc, realloc};
use servo_allocator::usable_size;
static FREETYPE_MEMORY_USAGE: AtomicUsize = AtomicUsize::new(0);
static FREETYPE_LIBRARY_HANDLE: OnceLock<Mutex<FreeTypeLibraryHandle>> = OnceLock::new();
static FREETYPE_LIBRARY_HANDLE: OnceLock<ReentrantMutex<FreeTypeLibraryHandle>> = OnceLock::new();
extern "C" fn ft_alloc(_: FT_Memory, req_size: c_long) -> *mut c_void {
unsafe {
@ -90,7 +90,7 @@ impl FreeTypeLibraryHandle {
/// > also, as long as a mutex lock is used around FT_New_Face and FT_Done_Face.
///
/// See <https://freetype.org/freetype2/docs/reference/ft2-library_setup.html>.
pub(crate) fn get() -> &'static Mutex<FreeTypeLibraryHandle> {
pub(crate) fn get() -> &'static ReentrantMutex<FreeTypeLibraryHandle> {
FREETYPE_LIBRARY_HANDLE.get_or_init(|| {
let freetype_memory = Box::into_raw(Box::new(FT_MemoryRec {
user: ptr::null_mut(),
@ -105,7 +105,7 @@ impl FreeTypeLibraryHandle {
panic!("Unable to initialize FreeType library");
}
FT_Add_Default_Modules(freetype_library);
Mutex::new(FreeTypeLibraryHandle {
ReentrantMutex::new(FreeTypeLibraryHandle {
freetype_library,
freetype_memory,
})

View file

@ -14,6 +14,7 @@ use style::Atom;
use webrender_api::NativeFontHandle;
pub mod font;
mod freetype_face;
#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(ohos_mock)))]
pub mod font_list;