Skip to content

Commit 3d13df6

Browse files
committed
Make PCG thread safe
1 parent 2b97feb commit 3d13df6

7 files changed

Lines changed: 376 additions & 78 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pecos-clib-pcg/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ readme = "README.md"
1515

1616
[build-dependencies]
1717
cc.workspace = true
18-
ureq = { version = "2.9", default-features = false, features = ["native-tls"] }
1918

2019
[lints]
2120
workspace = true

crates/pecos-clib-pcg/build.rs

Lines changed: 14 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,23 @@
11
use cc::Build;
22
use std::env;
3-
use std::fs;
4-
use std::path::{Path, PathBuf};
3+
use std::path::PathBuf;
54

6-
// TODO: Should probably just vendor the C code into the Rust crate...
75
fn main() {
8-
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
96
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
10-
11-
// Try local path first (for development)
12-
let local_clib_path = manifest_dir
13-
.parent()
14-
.unwrap()
15-
.parent()
16-
.unwrap()
17-
.join("clib")
18-
.join("pecos-rng");
19-
20-
let src_path = if local_clib_path.exists() {
21-
// Development: use local files
22-
let src = local_clib_path.join("src");
23-
println!("cargo:rerun-if-changed={}", src.join("rng_pcg.c").display());
24-
println!("cargo:rerun-if-changed={}", src.join("rng_pcg.h").display());
25-
src
26-
} else {
27-
// Published crate: download from GitHub
28-
let pcg_dir = out_dir.join("pcg");
29-
fs::create_dir_all(&pcg_dir).unwrap();
30-
31-
let commit = "95a6ddbdf85ad7bcf8b9133aa2552f3f1ae7da84";
32-
let base_url = format!(
33-
"https://raw.githubusercontent.com/PECOS-packages/PECOS/{commit}/clib/pecos-rng/src"
34-
);
35-
36-
// Download files if they don't exist
37-
download_if_needed(&pcg_dir.join("rng_pcg.c"), &format!("{base_url}/rng_pcg.c"));
38-
download_if_needed(&pcg_dir.join("rng_pcg.h"), &format!("{base_url}/rng_pcg.h"));
39-
40-
pcg_dir
41-
};
7+
let c_src_dir = manifest_dir.join("c_src");
8+
9+
// Build our local C code with thread-safe functions exposed
10+
println!(
11+
"cargo:rerun-if-changed={}",
12+
c_src_dir.join("rng_pcg.c").display()
13+
);
14+
println!(
15+
"cargo:rerun-if-changed={}",
16+
c_src_dir.join("rng_pcg.h").display()
17+
);
4218

4319
Build::new()
44-
.file(src_path.join("rng_pcg.c"))
45-
.include(&src_path)
20+
.file(c_src_dir.join("rng_pcg.c"))
21+
.include(&c_src_dir)
4622
.compile("pecos_pcg");
4723
}
48-
49-
fn download_if_needed(path: &Path, url: &str) {
50-
if !path.exists() {
51-
println!("cargo:warning=Downloading {} to {}", url, path.display());
52-
53-
let response = ureq::get(url)
54-
.call()
55-
.unwrap_or_else(|e| panic!("Failed to download {url}: {e}"));
56-
57-
let mut file = fs::File::create(path)
58-
.unwrap_or_else(|e| panic!("Failed to create {}: {}", path.display(), e));
59-
60-
std::io::copy(&mut response.into_reader(), &mut file)
61-
.unwrap_or_else(|e| panic!("Failed to write {}: {}", path.display(), e));
62-
}
63-
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* PCG Random Number Generation for C.
3+
*
4+
* Copyright 2014 Melissa O'Neill <oneill@pcg-random.org>
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* For additional information about the PCG random number generation scheme,
19+
* including its license and other licensing options, visit
20+
*
21+
* http://www.pcg-random.org
22+
*/
23+
24+
#include <math.h>
25+
#include "rng_pcg.h"
26+
27+
// RNG state structure is now defined in the header
28+
29+
// global RNG state
30+
static pcg32_random_t pcg32_global = {
31+
0x853c49e6748fea9bULL,
32+
0xda3e39cb94b95bdbULL
33+
};
34+
35+
// default multi[plier]
36+
#define PCG_DEFAULT_MULTIPLIER_64 6364136223846793005ULL
37+
38+
// helper functions
39+
static inline uint32_t pcg_rotr_32(uint32_t value, unsigned int urot) {
40+
int rot = (int)urot;
41+
return (value >> rot) | (value << ((-rot) & 31));
42+
}
43+
44+
static inline void pcg_setseq_64_step_r(pcg32_random_t* rng) {
45+
rng->state = rng->state * PCG_DEFAULT_MULTIPLIER_64 + rng->inc;
46+
}
47+
48+
static inline uint32_t pcg_output_xsh_rr_64_32(uint64_t state) {
49+
return pcg_rotr_32(((state >> 18u) ^ state) >> 27u, state >> 59u);
50+
}
51+
52+
uint32_t pcg32_random_r(pcg32_random_t* rng) {
53+
const uint64_t oldstate = rng->state;
54+
pcg_setseq_64_step_r(rng);
55+
return pcg_output_xsh_rr_64_32(oldstate);
56+
}
57+
58+
uint32_t pcg32_boundedrand_r(pcg32_random_t* rng, uint32_t ubound) {
59+
int32_t bound = (int32_t)ubound;
60+
uint32_t threshold = -bound % bound;
61+
for (;;) {
62+
const uint32_t r = pcg32_random_r(rng);
63+
if (r >= threshold)
64+
return r % bound;
65+
}
66+
}
67+
68+
void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq) {
69+
rng->state = 0U;
70+
rng->inc = (initseq << 1u) | 1u;
71+
pcg_setseq_64_step_r(rng);
72+
rng->state += initstate;
73+
pcg_setseq_64_step_r(rng);
74+
}
75+
76+
// public interface to RNG
77+
78+
uint32_t pcg32_random() {
79+
return pcg32_random_r(&pcg32_global);
80+
}
81+
82+
uint32_t pcg32_boundedrand(uint32_t bound) {
83+
return pcg32_boundedrand_r(&pcg32_global, bound);
84+
}
85+
86+
double pcg32_frandom() {
87+
return ldexp(pcg32_random(), -32);
88+
}
89+
90+
void pcg32_srandom(uint64_t seq) {
91+
pcg32_srandom_r(&pcg32_global, 42u, seq);
92+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* PCG Random Number Generation for C.
3+
*
4+
* Copyright 2014 Melissa O'Neill <oneill@pcg-random.org>
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* For additional information about the PCG random number generation scheme,
19+
* including its license and other licensing options, visit
20+
*
21+
* http://www.pcg-random.org
22+
*/
23+
24+
#pragma once
25+
26+
#include <stdint.h>
27+
28+
#if __cplusplus
29+
extern "C" {
30+
#endif
31+
32+
// Thread-safe RNG state structure
33+
typedef struct pcg_state_setseq_64 {
34+
uint64_t state;
35+
uint64_t inc;
36+
} pcg32_random_t;
37+
38+
// Thread-safe versions that take explicit state
39+
uint32_t pcg32_random_r(pcg32_random_t* rng);
40+
uint32_t pcg32_boundedrand_r(pcg32_random_t* rng, uint32_t bound);
41+
void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq);
42+
43+
// Global state versions (for compatibility)
44+
uint32_t pcg32_random();
45+
uint32_t pcg32_boundedrand(uint32_t bound);
46+
double pcg32_frandom();
47+
void pcg32_srandom(uint64_t seq);
48+
49+
#if __cplusplus
50+
}
51+
#endif

crates/pecos-clib-pcg/src/lib.rs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,81 @@
1-
// FFI bindings to the C PCG library
1+
use std::cell::RefCell;
2+
3+
// Mirror the C struct pcg32_random_t
4+
#[repr(C)]
5+
struct PcgState {
6+
state: u64,
7+
inc: u64,
8+
}
9+
10+
// FFI bindings to the C PCG library - now with thread-safe versions!
211
unsafe extern "C" {
3-
fn pcg32_random() -> u32;
4-
fn pcg32_boundedrand(bound: u32) -> u32;
5-
fn pcg32_frandom() -> f64;
6-
fn pcg32_srandom(seq: u64);
12+
fn pcg32_random_r(rng: *mut PcgState) -> u32;
13+
fn pcg32_boundedrand_r(rng: *mut PcgState, bound: u32) -> u32;
14+
fn pcg32_srandom_r(rng: *mut PcgState, initstate: u64, initseq: u64);
15+
}
16+
17+
// Thread-local state: each thread has its own PCG state
18+
thread_local! {
19+
static THREAD_STATE: RefCell<ThreadRngState> = RefCell::new(ThreadRngState::new());
20+
}
21+
22+
struct ThreadRngState {
23+
pcg: PcgState,
24+
seed: u64,
25+
}
26+
27+
impl ThreadRngState {
28+
fn new() -> Self {
29+
let mut state = Self {
30+
pcg: PcgState { state: 0, inc: 0 },
31+
seed: 0,
32+
};
33+
// Initialize with default seed
34+
state.reseed(42);
35+
state
36+
}
37+
38+
fn reseed(&mut self, seed: u64) {
39+
self.seed = seed;
40+
unsafe {
41+
// For PCG: initstate affects starting position, initseq selects sequence
42+
// Using seed for initseq (sequence selection) and a fixed initstate
43+
// matches the original behavior while allowing different sequences per thread
44+
pcg32_srandom_r(&raw mut self.pcg, 42, seed);
45+
}
46+
}
747
}
848

949
// Rust wrapper functions with safe interfaces
1050
#[must_use]
1151
pub fn random() -> u32 {
12-
unsafe { pcg32_random() }
52+
THREAD_STATE.with(|state| {
53+
let mut state = state.borrow_mut();
54+
unsafe { pcg32_random_r(&raw mut state.pcg) }
55+
})
1356
}
1457

1558
#[must_use]
1659
pub fn boundedrand(bound: u32) -> u32 {
17-
unsafe { pcg32_boundedrand(bound) }
60+
THREAD_STATE.with(|state| {
61+
let mut state = state.borrow_mut();
62+
unsafe { pcg32_boundedrand_r(&raw mut state.pcg, bound) }
63+
})
1864
}
1965

2066
#[must_use]
2167
pub fn frandom() -> f64 {
22-
unsafe { pcg32_frandom() }
68+
// The C code implements this as ldexp(pcg32_random(), -32)
69+
// which is equivalent to dividing by 2^32
70+
THREAD_STATE.with(|state| {
71+
let mut state = state.borrow_mut();
72+
let val = unsafe { pcg32_random_r(&raw mut state.pcg) };
73+
f64::from(val) * 2.0_f64.powi(-32)
74+
})
2375
}
2476

2577
pub fn srandom(seq: u64) {
26-
unsafe { pcg32_srandom(seq) }
78+
THREAD_STATE.with(|state| state.borrow_mut().reseed(seq));
2779
}
2880

2981
#[cfg(test)]

0 commit comments

Comments
 (0)