Skip to content

Commit b53819b

Browse files
committed
feat: add stable API functions for integer and symbol conversions
This commit adds comprehensive integer and symbol conversion support to the stable API, enabling safe Ruby <-> C type conversions across all Ruby versions. Integer Conversions: - FIX2LONG/FIX2ULONG - Extract values from Fixnum - LONG2FIX - Create Fixnum from long (with range check via FIXABLE) - NUM2LONG/NUM2ULONG - Convert any Integer (Fixnum or Bignum) to C types - LONG2NUM/ULONG2NUM - Create Integer from C types (Fixnum or Bignum) - FIXABLE/POSFIXABLE - Range checking for safe Fixnum creation Symbol Conversions: - ID2SYM/RB_ID2SYM - Convert ID to Symbol - SYM2ID/RB_SYM2ID - Convert Symbol to ID Implementation: - Added trait methods to StableApiDefinition - Rust implementations for all Ruby versions (2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 4.0) - C fallback implementations via compiled.c - Public macros in macros.rs mirroring Ruby C API - Comprehensive test coverage (128 tests) - Performance analysis support via enhanced show-asm script All implementations maintain parity between Rust and compiled C versions, verified through extensive parity testing.
1 parent d6ef5d9 commit b53819b

File tree

12 files changed

+1158
-20
lines changed

12 files changed

+1158
-20
lines changed

crates/rb-sys-tests/src/stable_api_test.rs

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,3 +861,398 @@ parity_test!(
861861
}
862862
}
863863
);
864+
865+
// Integer conversion tests - fix2long
866+
parity_test!(
867+
name: test_fix2long_zero,
868+
func: fix2long,
869+
data_factory: { ruby_eval!("0") },
870+
expected: 0 as std::os::raw::c_long
871+
);
872+
873+
parity_test!(
874+
name: test_fix2long_one,
875+
func: fix2long,
876+
data_factory: { ruby_eval!("1") },
877+
expected: 1 as std::os::raw::c_long
878+
);
879+
880+
parity_test!(
881+
name: test_fix2long_negative_one,
882+
func: fix2long,
883+
data_factory: { ruby_eval!("-1") },
884+
expected: -1 as std::os::raw::c_long
885+
);
886+
887+
parity_test!(
888+
name: test_fix2long_positive_small,
889+
func: fix2long,
890+
data_factory: { ruby_eval!("42") },
891+
expected: 42 as std::os::raw::c_long
892+
);
893+
894+
parity_test!(
895+
name: test_fix2long_negative_small,
896+
func: fix2long,
897+
data_factory: { ruby_eval!("-123") },
898+
expected: -123 as std::os::raw::c_long
899+
);
900+
901+
parity_test!(
902+
name: test_fix2long_large_positive,
903+
func: fix2long,
904+
data_factory: { ruby_eval!("1073741823") }, // 2^30 - 1
905+
expected: 1073741823 as std::os::raw::c_long
906+
);
907+
908+
parity_test!(
909+
name: test_fix2long_large_negative,
910+
func: fix2long,
911+
data_factory: { ruby_eval!("-1073741824") }, // -2^30
912+
expected: -1073741824 as std::os::raw::c_long
913+
);
914+
915+
// 64-bit specific tests (c_long is i64 on Unix)
916+
#[cfg(not(windows))]
917+
parity_test!(
918+
name: test_fix2long_max_fixnum,
919+
func: fix2long,
920+
data_factory: { ruby_eval!("(2 ** 62) - 1") }, // FIXNUM_MAX on 64-bit
921+
expected: ((1i64 << 62) - 1) as std::os::raw::c_long
922+
);
923+
924+
#[cfg(not(windows))]
925+
parity_test!(
926+
name: test_fix2long_min_fixnum,
927+
func: fix2long,
928+
data_factory: { ruby_eval!("-(2 ** 62)") }, // FIXNUM_MIN on 64-bit
929+
expected: (-(1i64 << 62)) as std::os::raw::c_long
930+
);
931+
932+
// Windows-specific tests (c_long is i32 on Windows)
933+
#[cfg(windows)]
934+
parity_test!(
935+
name: test_fix2long_max_fixnum,
936+
func: fix2long,
937+
data_factory: { ruby_eval!("(2 ** 30) - 1") }, // FIXNUM_MAX on 32-bit
938+
expected: ((1i32 << 30) - 1) as std::os::raw::c_long
939+
);
940+
941+
#[cfg(windows)]
942+
parity_test!(
943+
name: test_fix2long_min_fixnum,
944+
func: fix2long,
945+
data_factory: { ruby_eval!("-(2 ** 30)") }, // FIXNUM_MIN on 32-bit
946+
expected: (-(1i32 << 30)) as std::os::raw::c_long
947+
);
948+
949+
// Integer conversion tests - fix2ulong
950+
parity_test!(
951+
name: test_fix2ulong_zero,
952+
func: fix2ulong,
953+
data_factory: { ruby_eval!("0") },
954+
expected: 0 as std::os::raw::c_ulong
955+
);
956+
957+
parity_test!(
958+
name: test_fix2ulong_one,
959+
func: fix2ulong,
960+
data_factory: { ruby_eval!("1") },
961+
expected: 1 as std::os::raw::c_ulong
962+
);
963+
964+
parity_test!(
965+
name: test_fix2ulong_small,
966+
func: fix2ulong,
967+
data_factory: { ruby_eval!("42") },
968+
expected: 42 as std::os::raw::c_ulong
969+
);
970+
971+
parity_test!(
972+
name: test_fix2ulong_medium,
973+
func: fix2ulong,
974+
data_factory: { ruby_eval!("1000") },
975+
expected: 1000 as std::os::raw::c_ulong
976+
);
977+
978+
parity_test!(
979+
name: test_fix2ulong_large,
980+
func: fix2ulong,
981+
data_factory: { ruby_eval!("1073741823") }, // 2^30 - 1
982+
expected: 1073741823 as std::os::raw::c_ulong
983+
);
984+
985+
// 64-bit specific test
986+
#[cfg(not(windows))]
987+
parity_test!(
988+
name: test_fix2ulong_max_fixnum,
989+
func: fix2ulong,
990+
data_factory: { ruby_eval!("(2 ** 62) - 1") }, // FIXNUM_MAX on 64-bit
991+
expected: ((1u64 << 62) - 1) as std::os::raw::c_ulong
992+
);
993+
994+
// Windows-specific test
995+
#[cfg(windows)]
996+
parity_test!(
997+
name: test_fix2ulong_max_fixnum,
998+
func: fix2ulong,
999+
data_factory: { ruby_eval!("(2 ** 30) - 1") }, // FIXNUM_MAX on 32-bit
1000+
expected: ((1u32 << 30) - 1) as std::os::raw::c_ulong
1001+
);
1002+
1003+
// Integer conversion tests - num2long (handles both fixnum and bignum)
1004+
parity_test!(
1005+
name: test_num2long_zero,
1006+
func: num2long,
1007+
data_factory: { ruby_eval!("0") },
1008+
expected: 0 as std::os::raw::c_long
1009+
);
1010+
1011+
parity_test!(
1012+
name: test_num2long_one,
1013+
func: num2long,
1014+
data_factory: { ruby_eval!("1") },
1015+
expected: 1 as std::os::raw::c_long
1016+
);
1017+
1018+
parity_test!(
1019+
name: test_num2long_negative_one,
1020+
func: num2long,
1021+
data_factory: { ruby_eval!("-1") },
1022+
expected: -1 as std::os::raw::c_long
1023+
);
1024+
1025+
parity_test!(
1026+
name: test_num2long_positive_small,
1027+
func: num2long,
1028+
data_factory: { ruby_eval!("999") },
1029+
expected: 999 as std::os::raw::c_long
1030+
);
1031+
1032+
parity_test!(
1033+
name: test_num2long_negative_small,
1034+
func: num2long,
1035+
data_factory: { ruby_eval!("-999") },
1036+
expected: -999 as std::os::raw::c_long
1037+
);
1038+
1039+
parity_test!(
1040+
name: test_num2long_large_positive,
1041+
func: num2long,
1042+
data_factory: { ruby_eval!("2147483647") }, // 2^31 - 1 (i32 max)
1043+
expected: 2147483647 as std::os::raw::c_long
1044+
);
1045+
1046+
parity_test!(
1047+
name: test_num2long_large_negative,
1048+
func: num2long,
1049+
data_factory: { ruby_eval!("-2147483648") }, // -2^31 (i32 min)
1050+
expected: -2147483648 as std::os::raw::c_long
1051+
);
1052+
1053+
// Integer conversion tests - num2ulong
1054+
parity_test!(
1055+
name: test_num2ulong_zero,
1056+
func: num2ulong,
1057+
data_factory: { ruby_eval!("0") },
1058+
expected: 0 as std::os::raw::c_ulong
1059+
);
1060+
1061+
parity_test!(
1062+
name: test_num2ulong_one,
1063+
func: num2ulong,
1064+
data_factory: { ruby_eval!("1") },
1065+
expected: 1 as std::os::raw::c_ulong
1066+
);
1067+
1068+
parity_test!(
1069+
name: test_num2ulong_small,
1070+
func: num2ulong,
1071+
data_factory: { ruby_eval!("888") },
1072+
expected: 888 as std::os::raw::c_ulong
1073+
);
1074+
1075+
parity_test!(
1076+
name: test_num2ulong_large,
1077+
func: num2ulong,
1078+
data_factory: { ruby_eval!("4294967295") }, // 2^32 - 1 (u32 max)
1079+
expected: 4294967295 as std::os::raw::c_ulong
1080+
);
1081+
1082+
// long2num/ulong2num parity tests
1083+
// These functions take a primitive and return VALUE, so we need a different pattern
1084+
macro_rules! parity_test_long2num {
1085+
(name: $name:ident, value: $value:expr) => {
1086+
#[rb_sys_test_helpers::ruby_test]
1087+
fn $name() {
1088+
use rb_sys::stable_api;
1089+
let val: std::os::raw::c_long = $value;
1090+
1091+
let rust_result = stable_api::get_default().long2num(val);
1092+
let compiled_c_result = stable_api::get_compiled().long2num(val);
1093+
1094+
// Compare by converting back to long
1095+
let rust_roundtrip = unsafe { stable_api::get_default().num2long(rust_result) };
1096+
let compiled_roundtrip =
1097+
unsafe { stable_api::get_compiled().num2long(compiled_c_result) };
1098+
1099+
assert_eq!(
1100+
rust_roundtrip, compiled_roundtrip,
1101+
"long2num parity failed for {}: rust={}, compiled={}",
1102+
val, rust_roundtrip, compiled_roundtrip
1103+
);
1104+
assert_eq!(rust_roundtrip, val, "roundtrip failed for {}", val);
1105+
}
1106+
};
1107+
}
1108+
1109+
macro_rules! parity_test_ulong2num {
1110+
(name: $name:ident, value: $value:expr) => {
1111+
#[rb_sys_test_helpers::ruby_test]
1112+
fn $name() {
1113+
use rb_sys::stable_api;
1114+
let val: std::os::raw::c_ulong = $value;
1115+
1116+
let rust_result = stable_api::get_default().ulong2num(val);
1117+
let compiled_c_result = stable_api::get_compiled().ulong2num(val);
1118+
1119+
// Compare by converting back to ulong
1120+
let rust_roundtrip = unsafe { stable_api::get_default().num2ulong(rust_result) };
1121+
let compiled_roundtrip =
1122+
unsafe { stable_api::get_compiled().num2ulong(compiled_c_result) };
1123+
1124+
assert_eq!(
1125+
rust_roundtrip, compiled_roundtrip,
1126+
"ulong2num parity failed for {}: rust={}, compiled={}",
1127+
val, rust_roundtrip, compiled_roundtrip
1128+
);
1129+
assert_eq!(rust_roundtrip, val, "roundtrip failed for {}", val);
1130+
}
1131+
};
1132+
}
1133+
1134+
// long2num parity tests
1135+
parity_test_long2num!(name: test_long2num_zero, value: 0);
1136+
parity_test_long2num!(name: test_long2num_one, value: 1);
1137+
parity_test_long2num!(name: test_long2num_negative_one, value: -1);
1138+
parity_test_long2num!(name: test_long2num_small_positive, value: 12345);
1139+
parity_test_long2num!(name: test_long2num_small_negative, value: -12345);
1140+
parity_test_long2num!(name: test_long2num_large_positive, value: 2147483647); // i32 max
1141+
parity_test_long2num!(name: test_long2num_large_negative, value: -2147483648); // i32 min
1142+
1143+
// 64-bit specific long2num tests
1144+
#[cfg(not(windows))]
1145+
parity_test_long2num!(name: test_long2num_very_large_positive, value: 4611686018427387903); // 2^62 - 1 (near FIXNUM_MAX)
1146+
#[cfg(not(windows))]
1147+
parity_test_long2num!(name: test_long2num_very_large_negative, value: -4611686018427387904); // -2^62 (near FIXNUM_MIN)
1148+
1149+
// Windows-specific long2num tests
1150+
#[cfg(windows)]
1151+
parity_test_long2num!(name: test_long2num_very_large_positive, value: 1073741823); // 2^30 - 1 (near FIXNUM_MAX on 32-bit)
1152+
#[cfg(windows)]
1153+
parity_test_long2num!(name: test_long2num_very_large_negative, value: -1073741824); // -2^30 (near FIXNUM_MIN on 32-bit)
1154+
1155+
// ulong2num parity tests
1156+
parity_test_ulong2num!(name: test_ulong2num_zero, value: 0);
1157+
parity_test_ulong2num!(name: test_ulong2num_one, value: 1);
1158+
parity_test_ulong2num!(name: test_ulong2num_small, value: 54321);
1159+
parity_test_ulong2num!(name: test_ulong2num_large, value: 4294967295); // u32 max
1160+
1161+
// 64-bit specific ulong2num test
1162+
#[cfg(not(windows))]
1163+
parity_test_ulong2num!(name: test_ulong2num_very_large, value: 4611686018427387903); // 2^62 - 1 (near FIXNUM_MAX)
1164+
1165+
// Windows-specific ulong2num test
1166+
#[cfg(windows)]
1167+
parity_test_ulong2num!(name: test_ulong2num_very_large, value: 1073741823); // 2^30 - 1 (near FIXNUM_MAX on 32-bit)
1168+
1169+
// fixable/posfixable parity tests
1170+
#[rb_sys_test_helpers::ruby_test]
1171+
fn test_fixable_parity() {
1172+
use rb_sys::stable_api;
1173+
let rust_api = stable_api::get_default();
1174+
let compiled_api = stable_api::get_compiled();
1175+
1176+
#[cfg(not(windows))]
1177+
const TEST_VALUES: &[std::os::raw::c_long] = &[
1178+
0,
1179+
1,
1180+
-1,
1181+
100,
1182+
-100,
1183+
1000,
1184+
-1000,
1185+
2147483647, // i32 max
1186+
-2147483648, // i32 min
1187+
4611686018427387903, // 2^62 - 1 (FIXNUM_MAX on 64-bit)
1188+
-4611686018427387904, // -2^62 (FIXNUM_MIN on 64-bit)
1189+
4611686018427387904, // 2^62 (just above FIXNUM_MAX, not fixable)
1190+
-4611686018427387905, // just below FIXNUM_MIN (not fixable)
1191+
];
1192+
1193+
#[cfg(windows)]
1194+
const TEST_VALUES: &[std::os::raw::c_long] = &[
1195+
0,
1196+
1,
1197+
-1,
1198+
100,
1199+
-100,
1200+
1000,
1201+
-1000,
1202+
1073741823, // 2^30 - 1 (FIXNUM_MAX on 32-bit)
1203+
-1073741824, // -2^30 (FIXNUM_MIN on 32-bit)
1204+
1073741824, // 2^30 (just above FIXNUM_MAX, not fixable)
1205+
-1073741825, // just below FIXNUM_MIN (not fixable)
1206+
2147483647, // i32 max (not fixable)
1207+
-2147483648, // i32 min (not fixable)
1208+
];
1209+
1210+
for &val in TEST_VALUES {
1211+
let rust_result = rust_api.fixable(val);
1212+
let compiled_result = compiled_api.fixable(val);
1213+
assert_eq!(
1214+
rust_result, compiled_result,
1215+
"fixable parity failed for {}: rust={}, compiled={}",
1216+
val, rust_result, compiled_result
1217+
);
1218+
}
1219+
}
1220+
1221+
#[rb_sys_test_helpers::ruby_test]
1222+
fn test_posfixable_parity() {
1223+
use rb_sys::stable_api;
1224+
let rust_api = stable_api::get_default();
1225+
let compiled_api = stable_api::get_compiled();
1226+
1227+
#[cfg(not(windows))]
1228+
const TEST_VALUES: &[std::os::raw::c_ulong] = &[
1229+
0,
1230+
1,
1231+
100,
1232+
1000,
1233+
2147483647, // i32 max
1234+
4294967295, // u32 max
1235+
4611686018427387903, // 2^62 - 1 (FIXNUM_MAX on 64-bit)
1236+
4611686018427387904, // 2^62 (just above FIXNUM_MAX)
1237+
9223372036854775807, // i64 max as u64
1238+
18446744073709551615, // u64 max
1239+
];
1240+
1241+
#[cfg(windows)]
1242+
const TEST_VALUES: &[std::os::raw::c_ulong] = &[
1243+
0, 1, 100, 1000, 1073741823, // 2^30 - 1 (FIXNUM_MAX on 32-bit)
1244+
1073741824, // 2^30 (just above FIXNUM_MAX)
1245+
2147483647, // i32 max
1246+
4294967295, // u32 max
1247+
];
1248+
1249+
for &val in TEST_VALUES {
1250+
let rust_result = rust_api.posfixable(val);
1251+
let compiled_result = compiled_api.posfixable(val);
1252+
assert_eq!(
1253+
rust_result, compiled_result,
1254+
"posfixable parity failed for {}: rust={}, compiled={}",
1255+
val, rust_result, compiled_result
1256+
);
1257+
}
1258+
}

0 commit comments

Comments
 (0)