Add eventfd file type and system call
This commit is contained in:
parent
f7ce60e764
commit
65694815a4
@ -112,5 +112,10 @@ enclave {
|
||||
[out] int* msg_flags_recv,
|
||||
int flags
|
||||
) propagate_errno;
|
||||
|
||||
int occlum_ocall_eventfd(
|
||||
unsigned int initval,
|
||||
int flags
|
||||
) propagate_errno;
|
||||
};
|
||||
};
|
||||
|
108
src/libos/src/fs/event_file.rs
Normal file
108
src/libos/src/fs/event_file.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use super::*;
|
||||
|
||||
/// Native Linux eventfd
|
||||
// TODO: move the implementaion of eventfd into libos to defend against Iago attacks from OCalls
|
||||
#[derive(Debug)]
|
||||
pub struct EventFile {
|
||||
host_fd: c_int,
|
||||
}
|
||||
|
||||
impl EventFile {
|
||||
pub fn new(init_val: u32, flags: EventCreationFlags) -> Result<Self> {
|
||||
let host_fd = try_libc!({
|
||||
let mut ret: i32 = 0;
|
||||
let status = occlum_ocall_eventfd(&mut ret, init_val, flags.bits());
|
||||
assert!(status == sgx_status_t::SGX_SUCCESS);
|
||||
ret
|
||||
});
|
||||
Ok(Self { host_fd })
|
||||
}
|
||||
|
||||
pub fn get_host_fd(&self) -> c_int {
|
||||
self.host_fd
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct EventCreationFlags: i32 {
|
||||
/// Provides semaphore-like semantics for reads from the new file descriptor
|
||||
const EFD_SEMAPHORE = 1 << 0;
|
||||
/// Non-blocking
|
||||
const EFD_NONBLOCK = 1 << 11;
|
||||
/// Close on exec
|
||||
const EFD_CLOEXEC = 1 << 19;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn occlum_ocall_eventfd(ret: *mut i32, init_val: u32, flags: i32) -> sgx_status_t;
|
||||
}
|
||||
|
||||
impl Drop for EventFile {
|
||||
fn drop(&mut self) {
|
||||
let ret = unsafe { libc::ocall::close(self.host_fd) };
|
||||
assert!(ret == 0);
|
||||
}
|
||||
}
|
||||
|
||||
impl File for EventFile {
|
||||
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
||||
let ret = try_libc!(libc::ocall::read(
|
||||
self.host_fd,
|
||||
buf.as_mut_ptr() as *mut c_void,
|
||||
buf.len()
|
||||
)) as usize;
|
||||
assert!(ret <= buf.len());
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn write(&self, buf: &[u8]) -> Result<usize> {
|
||||
let ret = try_libc!(libc::ocall::write(
|
||||
self.host_fd,
|
||||
buf.as_ptr() as *const c_void,
|
||||
buf.len()
|
||||
)) as usize;
|
||||
assert!(ret <= buf.len());
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn get_access_mode(&self) -> Result<AccessMode> {
|
||||
Ok(AccessMode::O_RDWR)
|
||||
}
|
||||
|
||||
fn get_status_flags(&self) -> Result<StatusFlags> {
|
||||
let ret = try_libc!(libc::ocall::fcntl_arg0(self.get_host_fd(), libc::F_GETFL));
|
||||
Ok(StatusFlags::from_bits_truncate(ret as u32))
|
||||
}
|
||||
|
||||
fn set_status_flags(&self, new_status_flags: StatusFlags) -> Result<()> {
|
||||
let valid_flags_mask = StatusFlags::O_APPEND
|
||||
| StatusFlags::O_ASYNC
|
||||
| StatusFlags::O_DIRECT
|
||||
| StatusFlags::O_NOATIME
|
||||
| StatusFlags::O_NONBLOCK;
|
||||
let raw_status_flags = (new_status_flags & valid_flags_mask).bits();
|
||||
try_libc!(libc::ocall::fcntl_arg1(
|
||||
self.get_host_fd(),
|
||||
libc::F_SETFL,
|
||||
raw_status_flags as c_int
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsEvent {
|
||||
fn as_event(&self) -> Result<&EventFile>;
|
||||
}
|
||||
|
||||
impl AsEvent for FileRef {
|
||||
fn as_event(&self) -> Result<&EventFile> {
|
||||
self.as_any()
|
||||
.downcast_ref::<EventFile>()
|
||||
.ok_or_else(|| errno!(EBADF, "not an event file"))
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ use std::mem::MaybeUninit;
|
||||
use std::path::Path;
|
||||
|
||||
pub use self::dev_fs::AsDevRandom;
|
||||
pub use self::event_file::{AsEvent, EventFile};
|
||||
pub use self::file::{File, FileRef};
|
||||
pub use self::file_ops::{AccessMode, CreationFlags, Stat, StatusFlags};
|
||||
pub use self::file_ops::{Flock, FlockType};
|
||||
@ -22,6 +23,7 @@ pub use self::stdio::{StdinFile, StdoutFile};
|
||||
pub use self::syscalls::*;
|
||||
|
||||
mod dev_fs;
|
||||
mod event_file;
|
||||
mod file;
|
||||
mod file_ops;
|
||||
mod file_table;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use super::event_file::EventCreationFlags;
|
||||
use super::file_ops;
|
||||
use super::file_ops::{
|
||||
AccessibilityCheckFlags, AccessibilityCheckMode, DirFd, FcntlCmd, StatFlags,
|
||||
@ -12,6 +13,26 @@ pub struct iovec_t {
|
||||
len: size_t,
|
||||
}
|
||||
|
||||
pub fn do_eventfd2(init_val: u32, flags: i32) -> Result<isize> {
|
||||
info!("eventfd: initval {}, flags {} ", init_val, flags);
|
||||
|
||||
let inner_flags =
|
||||
EventCreationFlags::from_bits(flags).ok_or_else(|| errno!(EINVAL, "invalid flags"))?;
|
||||
let file_ref: Arc<Box<dyn File>> = {
|
||||
let event = EventFile::new(init_val, inner_flags)?;
|
||||
Arc::new(Box::new(event))
|
||||
};
|
||||
|
||||
let current_ref = process::get_current();
|
||||
let mut proc = current_ref.lock().unwrap();
|
||||
|
||||
let fd = proc.get_files().lock().unwrap().put(
|
||||
file_ref,
|
||||
inner_flags.contains(EventCreationFlags::EFD_CLOEXEC),
|
||||
);
|
||||
Ok(fd as isize)
|
||||
}
|
||||
|
||||
pub fn do_open(path: *const i8, flags: u32, mode: u32) -> Result<isize> {
|
||||
let path = from_user::clone_cstring_safely(path)?
|
||||
.to_string_lossy()
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use fs::{AsDevRandom, File, FileDesc, FileRef};
|
||||
use fs::{AsDevRandom, AsEvent, File, FileDesc, FileRef};
|
||||
use std::any::Any;
|
||||
use std::collections::btree_map::BTreeMap;
|
||||
use std::fmt;
|
||||
@ -25,6 +25,7 @@ pub fn do_select(
|
||||
let file_table_ref = proc.get_files().lock().unwrap();
|
||||
|
||||
for fd in 0..nfds {
|
||||
let fd_ref = file_table_ref.get(fd as FileDesc)?;
|
||||
let (r, w, e) = (
|
||||
readfds.is_set(fd),
|
||||
writefds.is_set(fd),
|
||||
@ -33,7 +34,7 @@ pub fn do_select(
|
||||
if !(r || w || e) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(socket) = file_table_ref.get(fd as FileDesc)?.as_unix_socket() {
|
||||
if let Ok(socket) = fd_ref.as_unix_socket() {
|
||||
warn!("select unix socket is unimplemented, spin for read");
|
||||
readfds.clear();
|
||||
writefds.clear();
|
||||
@ -56,7 +57,13 @@ pub fn do_select(
|
||||
}
|
||||
return Ok(1);
|
||||
}
|
||||
let host_fd = file_table_ref.get(fd as FileDesc)?.as_socket()?.fd();
|
||||
let host_fd = if let Ok(socket) = fd_ref.as_socket() {
|
||||
socket.fd()
|
||||
} else if let Ok(eventfd) = fd_ref.as_event() {
|
||||
eventfd.get_host_fd()
|
||||
} else {
|
||||
return_errno!(EBADF, "unsupported file type");
|
||||
};
|
||||
|
||||
host_to_libos_fd[host_fd as usize] = fd;
|
||||
let mut events = 0;
|
||||
@ -132,6 +139,9 @@ pub fn do_poll(pollfds: &mut [libc::pollfd], timeout: c_int) -> Result<usize> {
|
||||
// convert libos fd to host fd in the copy to keep pollfds unchanged
|
||||
u_pollfds[i].fd = socket.fd();
|
||||
u_pollfds[i].revents = 0;
|
||||
} else if let Ok(eventfd) = file_ref.as_event() {
|
||||
u_pollfds[i].fd = eventfd.get_host_fd();
|
||||
u_pollfds[i].revents = 0;
|
||||
} else if let Ok(socket) = file_ref.as_unix_socket() {
|
||||
// FIXME: spin poll until can read (hack for php)
|
||||
while (pollfd.events & libc::POLLIN) != 0 && socket.poll()?.0 == false {
|
||||
@ -210,13 +220,16 @@ pub fn do_epoll_ctl(
|
||||
let mut epoll = file_ref.as_epoll()?.inner.lock().unwrap();
|
||||
|
||||
let fd_ref = file_table_ref.get(fd)?;
|
||||
let sock_result = fd_ref.as_socket();
|
||||
if sock_result.is_err() {
|
||||
//FIXME: workaround for grpc, other fd types including pipe should be supported
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let host_fd = sock_result.unwrap().fd() as FileDesc;
|
||||
let host_fd = if let Ok(socket) = fd_ref.as_socket() {
|
||||
socket.fd() as FileDesc
|
||||
} else if let Ok(eventfd) = fd_ref.as_event() {
|
||||
eventfd.get_host_fd() as FileDesc
|
||||
} else {
|
||||
warn!("unsupported file type");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
epoll.ctl(op, host_fd, event)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -232,7 +232,11 @@ pub fn do_epoll_pwait(
|
||||
timeout: c_int,
|
||||
sigmask: *const usize, //TODO:add sigset_t
|
||||
) -> Result<isize> {
|
||||
info!("epoll_pwait");
|
||||
if sigmask.is_null() {
|
||||
info!("epoll_wait");
|
||||
} else {
|
||||
info!("epoll_pwait")
|
||||
}
|
||||
//TODO:add signal support
|
||||
do_epoll_wait(epfd, events, maxevents, 0)
|
||||
do_epoll_wait(epfd, events, maxevents, timeout)
|
||||
}
|
||||
|
@ -221,6 +221,8 @@ pub extern "C" fn dispatch_syscall(
|
||||
|
||||
SysPipe => fs::do_pipe2(arg0 as *mut i32, 0),
|
||||
SysPipe2 => fs::do_pipe2(arg0 as *mut i32, arg1 as u32),
|
||||
SysEventfd => fs::do_eventfd2(arg0 as u32, 0),
|
||||
SysEventfd2 => fs::do_eventfd2(arg0 as u32, arg1 as i32),
|
||||
SysDup => fs::do_dup(arg0 as FileDesc),
|
||||
SysDup2 => fs::do_dup2(arg0 as FileDesc, arg1 as FileDesc),
|
||||
SysDup3 => fs::do_dup3(arg0 as FileDesc, arg1 as FileDesc, arg2 as u32),
|
||||
|
@ -1,6 +1,11 @@
|
||||
#include <unistd.h>
|
||||
#include "ocalls.h"
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
void occlum_ocall_sync(void) {
|
||||
sync();
|
||||
}
|
||||
|
||||
int occlum_ocall_eventfd(unsigned int initval, int flags) {
|
||||
return eventfd(initval, flags);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ TEST_DEPS := client data_sink
|
||||
TESTS ?= empty env hello_world malloc mmap file fs_perms getpid spawn sched pipe time \
|
||||
truncate readdir mkdir open stat link symlink tls pthread uname rlimit server \
|
||||
server_epoll unix_socket cout hostfs cpuid rdtsc device sleep exit_group \
|
||||
ioctl fcntl
|
||||
ioctl fcntl eventfd
|
||||
# Benchmarks: need to be compiled and run by bench-% target
|
||||
BENCHES := spawn_and_exit_latency pipe_throughput unix_socket_throughput
|
||||
|
||||
|
5
test/eventfd/Makefile
Normal file
5
test/eventfd/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
include ../test_common.mk
|
||||
|
||||
EXTRA_C_FLAGS := -Wno-incompatible-pointer-types-discards-qualifiers
|
||||
EXTRA_LINK_FLAGS :=
|
||||
BIN_ARGS :=
|
251
test/eventfd/main.c
Normal file
251
test/eventfd/main.c
Normal file
@ -0,0 +1,251 @@
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <spawn.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "test.h"
|
||||
|
||||
#define MAXEVENTS 1
|
||||
|
||||
// ============================================================================
|
||||
// Test cases
|
||||
// ============================================================================
|
||||
|
||||
int test_fcntl_get_flags() {
|
||||
int event_fd = eventfd(0, 0);
|
||||
if (event_fd < 0) {
|
||||
THROW_ERROR("failed to create an eventfd");
|
||||
}
|
||||
|
||||
if ((fcntl(event_fd, F_GETFL, 0) != O_RDWR)) {
|
||||
close(event_fd);
|
||||
THROW_ERROR("fcntl get flags failed");
|
||||
}
|
||||
|
||||
close(event_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_fcntl_set_flags() {
|
||||
int event_fd = eventfd(0, 0);
|
||||
if (event_fd < 0) {
|
||||
THROW_ERROR("failed to create an eventfd");
|
||||
}
|
||||
|
||||
fcntl(event_fd, F_SETFL, O_NONBLOCK);
|
||||
if ((fcntl(event_fd, F_GETFL, 0) != (O_NONBLOCK | O_RDWR))) {
|
||||
close(event_fd);
|
||||
THROW_ERROR("fcntl set flags failed");
|
||||
}
|
||||
|
||||
close(event_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_create_with_flags() {
|
||||
int event_fd = eventfd(0, EFD_NONBLOCK);
|
||||
if (event_fd < 0) {
|
||||
THROW_ERROR("failed to create an eventfd");
|
||||
}
|
||||
|
||||
if ((fcntl(event_fd, F_GETFL, 0) != (O_NONBLOCK | O_RDWR))) {
|
||||
close(event_fd);
|
||||
THROW_ERROR("create flags failed\n");
|
||||
}
|
||||
|
||||
close(event_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct thread_arg {
|
||||
pthread_t tid;
|
||||
int fd;
|
||||
uint64_t data;
|
||||
};
|
||||
|
||||
#define TEST_DATA 678
|
||||
#define CHILD_NUM 16
|
||||
|
||||
static void *thread_child(void *arg) {
|
||||
struct thread_arg *child_arg = arg;
|
||||
write(child_arg->fd, &(child_arg->data), sizeof(child_arg->data));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int create_child(struct thread_arg *arg) {
|
||||
pthread_attr_t attr;
|
||||
if (pthread_attr_init(&attr) != 0) {
|
||||
THROW_ERROR("failed to initialize attribute");
|
||||
}
|
||||
|
||||
if (pthread_create(&(arg->tid), &attr, &thread_child, arg) != 0) {
|
||||
if (pthread_attr_destroy(&attr) != 0) {
|
||||
THROW_ERROR("failed to destroy attr");
|
||||
}
|
||||
THROW_ERROR("failed to create the thread");
|
||||
}
|
||||
|
||||
if (pthread_attr_destroy(&attr) != 0) {
|
||||
THROW_ERROR("failed to destroy attr");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_read_write() {
|
||||
int event_fd = eventfd(0, 0);
|
||||
if (event_fd < 0) {
|
||||
THROW_ERROR("failed to create an eventfd");
|
||||
}
|
||||
|
||||
struct thread_arg child_arg[CHILD_NUM] = {0};
|
||||
|
||||
// Create child threads and send eventfd and data
|
||||
for (int i = 0; i < CHILD_NUM; i++) {
|
||||
child_arg[i].fd = event_fd;
|
||||
child_arg[i].data = TEST_DATA;
|
||||
if (create_child(&child_arg[i]) != 0) {
|
||||
close(event_fd);
|
||||
THROW_ERROR("failed to create children");
|
||||
}
|
||||
}
|
||||
|
||||
// Check the data sent from children
|
||||
uint64_t data_recv = 0;
|
||||
|
||||
do {
|
||||
uint64_t cur_data = 0;
|
||||
ssize_t len_recv = read(event_fd, &cur_data, sizeof(uint64_t));
|
||||
if (len_recv != sizeof(uint64_t)) {
|
||||
close(event_fd);
|
||||
THROW_ERROR("received length is not as expected");
|
||||
}
|
||||
data_recv += cur_data;
|
||||
} while (data_recv != TEST_DATA*CHILD_NUM);
|
||||
|
||||
close(event_fd);
|
||||
|
||||
for (int i = 0; i < CHILD_NUM; i++) {
|
||||
if (pthread_join(child_arg[i].tid, NULL) != 0) {
|
||||
THROW_ERROR("pthread_join");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_select_with_socket() {
|
||||
fd_set wfds;
|
||||
|
||||
struct timeval tv = { .tv_sec = 60, .tv_usec = 0 };
|
||||
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int event_fd = eventfd(0, 0);
|
||||
if (event_fd < 0 || sock < 0) {
|
||||
THROW_ERROR("failed to create files");
|
||||
}
|
||||
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(sock, &wfds);
|
||||
FD_SET(event_fd, &wfds);
|
||||
|
||||
if (select(sock > event_fd? sock + 1: event_fd + 1, NULL, &wfds, NULL, &tv) <= 0) {
|
||||
close_files(2, sock, event_fd);
|
||||
THROW_ERROR("select failed");
|
||||
}
|
||||
|
||||
close_files(2, sock, event_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_poll_with_socket() {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int event_fd = eventfd(0, 0);
|
||||
if (event_fd < 0 || sock < 0) {
|
||||
THROW_ERROR("failed to create files");
|
||||
}
|
||||
|
||||
struct pollfd pollfds[] = {
|
||||
{ .fd = sock, .events = POLLIN, .revents = 0, },
|
||||
{ .fd = event_fd, .events = POLLOUT, .revents = 0 },
|
||||
};
|
||||
|
||||
int ret = poll(pollfds, 2, -1);
|
||||
if (ret <= 0) {
|
||||
close_files(2, event_fd, sock);
|
||||
THROW_ERROR("poll error");
|
||||
}
|
||||
|
||||
close_files(2, event_fd, sock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_epoll_with_socket() {
|
||||
int event_fd = eventfd(0, EFD_NONBLOCK);
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
int epfd = epoll_create1(0);
|
||||
|
||||
if (event_fd < 0 || sock < 0 || epfd < 0) {
|
||||
THROW_ERROR("failed to create files");
|
||||
}
|
||||
|
||||
struct epoll_event ctl_events[2] = {0};
|
||||
// Add eventfd to the interest list
|
||||
ctl_events[0].data.fd = event_fd;
|
||||
ctl_events[0].events = EPOLLIN | EPOLLET;
|
||||
// Add socket to the interest list
|
||||
ctl_events[1].data.fd = sock;
|
||||
ctl_events[1].events = EPOLLIN | EPOLLET;
|
||||
if (epoll_ctl(epfd, EPOLL_CTL_ADD, event_fd, &ctl_events[0]) == -1 ||
|
||||
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ctl_events[1]) == -1) {
|
||||
close_files(3, event_fd, sock, epfd);
|
||||
THROW_ERROR("epoll_ctl");
|
||||
}
|
||||
|
||||
struct thread_arg child_arg = { .tid = 0, .fd = event_fd, .data = TEST_DATA };
|
||||
if (create_child(&child_arg) != 0) {
|
||||
close_files(3, event_fd, sock, epfd);
|
||||
THROW_ERROR("failed to create child");
|
||||
}
|
||||
|
||||
struct epoll_event events[MAXEVENTS] = {0};
|
||||
if (epoll_pwait(epfd, events, MAXEVENTS, -1, NULL) <= 0){
|
||||
close_files(3, event_fd, sock, epfd);
|
||||
THROW_ERROR("epoll failed");
|
||||
}
|
||||
|
||||
close_files(3, event_fd, sock, epfd);
|
||||
|
||||
if (pthread_join(child_arg.tid, NULL) != 0) {
|
||||
THROW_ERROR("pthread_join");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
// ============================================================================
|
||||
// Test suite
|
||||
// ============================================================================
|
||||
|
||||
static test_case_t test_cases[] = {
|
||||
TEST_CASE(test_fcntl_get_flags),
|
||||
TEST_CASE(test_fcntl_set_flags),
|
||||
TEST_CASE(test_create_with_flags),
|
||||
TEST_CASE(test_read_write),
|
||||
TEST_CASE(test_epoll_with_socket),
|
||||
TEST_CASE(test_poll_with_socket),
|
||||
TEST_CASE(test_select_with_socket),
|
||||
};
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
return test_suite_run(test_cases, ARRAY_SIZE(test_cases));
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
#define __TEST_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#define _STR(x) #x
|
||||
#define STR(x) _STR(x)
|
||||
@ -37,4 +38,13 @@ int test_suite_run(test_case_t* test_cases, int num_test_cases) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void close_files(int count, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
close(va_arg(ap, int));
|
||||
}
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
#endif /* __TEST_H */
|
||||
|
Loading…
Reference in New Issue
Block a user