-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate test-suite for the low-level Storable
instances
#22
Comments
The cross-compilation story here is more complicated, if we want to be able to generate bindings for target B but run the test suite on target A; probably we simply don't want to support that, at least not initially. |
I am working with the I expect
Is this an issue, or am I doing something wrong? |
The Rust const _: () = {
["Size of bz_stream"][::std::mem::size_of::<bz_stream>() - 80usize];
["Alignment of bz_stream"][::std::mem::align_of::<bz_stream>() - 8usize];
["Offset of field: bz_stream::next_in"][::std::mem::offset_of!(bz_stream, next_in) - 0usize];
...
}; The tests are put inside of Each test looks like The following things are tested:
|
Based on https://juliainterop.github.io/Clang.jl/stable/api/ getOffsetOfField returns offset in bits, not bytes (and that makes sense when you consider packed structs) |
I see. I wonder if we should make this more explicit in our Hs implementation ( |
Great find, @phadej! Thanks! To confirm, I tested the following structure and indeed see the expected offsets of 0, 8, and 16 bits. struct foo {
char a;
char b;
char c;
}; I tested the following bit-field structure and get #include <stdbool.h>
#include <stdint.h>
struct datetime {
unsigned _BitInt(6) dt_seconds :6;
unsigned _BitInt(6) dt_minutes :6;
unsigned _BitInt(5) dt_hour :5;
unsigned _BitInt(5) dt_day :5;
unsigned _BitInt(4) dt_month :4;
bool dt_is_dst :1;
uint16_t dt_year;
}; This seems to be a C23 support issue. I tested the following C89 version and get the expected results. struct datetime {
unsigned dt_seconds :6;
unsigned dt_minutes :6;
unsigned dt_hour :5;
unsigned dt_day :5;
unsigned dt_month :4;
unsigned dt_is_dst :1;
unsigned dt_year;
}; In addition to documenting the bits unit of I imagine that we will need to handle the I am not sure if we should add an For now, I just did the following:
PR #275 |
I manually wrote tests to get a feel for the factors that we need to consider in the design. acme-c/acme.h#ifndef ACME_H
#define ACME_H
typedef struct foo {
char a;
int b;
float c;
} foo;
void acme_dump_foo(foo*);
#endif acme-c/acme.c#include <stdio.h>
#include "acme.h"
void acme_dump_foo(foo* x) {
printf("foo.a: %c\n", x->a);
printf("foo.b: %d\n", x->b);
printf("foo.c: %f\n", x->c);
printf("#foo (%02lu) %p\n", sizeof *x, x);
printf("#foo.a (%02lu) %p\n", sizeof x->a, &x->a);
printf("#foo.b (%02lu) %p\n", sizeof x->b, &x->b);
printf("#foo.c (%02lu) %p\n", sizeof x->c, &x->c);
} experiment-quickcheck.cabal
src/Acme.hs{-# LANGUAGE CApiFFI #-}
{-# LANGUAGE RecordWildCards #-}
module Acme
( CFoo(..)
, dumpFoo
) where
import Foreign as F
import qualified Foreign.C as FC
------------------------------------------------------------------------------
data CFoo = MkCFoo
{ cFoo_a :: FC.CChar
, cFoo_b :: FC.CInt
, cFoo_c :: FC.CFloat
}
deriving (Eq, Show)
instance F.Storable CFoo where
sizeOf _ = 12
alignment _ = 4
peek ptr = do
cFoo_a <- F.peekByteOff ptr 0
cFoo_b <- F.peekByteOff ptr 4
cFoo_c <- F.peekByteOff ptr 8
pure MkCFoo{..}
poke ptr MkCFoo{..} = do
F.pokeByteOff ptr 0 cFoo_a
F.pokeByteOff ptr 4 cFoo_b
F.pokeByteOff ptr 8 cFoo_c
------------------------------------------------------------------------------
dumpFoo :: CFoo -> IO ()
dumpFoo x = F.alloca $ \ptr -> do
F.poke ptr x
cDumpFoo ptr
foreign import capi "acme.h acme_dump_foo"
cDumpFoo :: F.Ptr CFoo -> IO () app/Main.hsmodule Main (main) where
import qualified Acme
------------------------------------------------------------------------------
main :: IO ()
main = Acme.dumpFoo $ Acme.MkCFoo 120 11 3.14159 test-hs-bindgen/cbits/test-acme.h#ifndef TEST_ACME_H
#define TEST_ACME_H
#include <stdbool.h>
#include "acme.h"
void test_CFoo_CToHs(char, int, float, foo*);
bool test_CFoo_HsToC(foo*, char, int, float);
#endif test-hs-bindgen/cbits/test-acme.c#include <stdbool.h>
#include "acme.h"
#include "test-acme.h"
void test_CFoo_CToHs(char a, int b, float c, foo* target) {
target->a = a;
target->b = b;
target->c = c;
}
bool test_CFoo_HsToC(foo* value, char a, int b, float c) {
return value->a == a && value->b == b && value->c == c;
} test-hs-bindgen/src/Spec.hsmodule Main (main) where
import Test.Tasty (defaultMain, testGroup)
import qualified Acme.Test
------------------------------------------------------------------------------
main :: IO ()
main = defaultMain $ testGroup "test-hs-bindgen"
[ Acme.Test.tests
] test-hs-bindgen/src/Acme/Test/Instances.hs{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Acme.Test.Instances () where
import qualified Acme
import qualified Test.QuickCheck as QC
------------------------------------------------------------------------------
instance QC.Arbitrary Acme.CFoo where
arbitrary = do
cFoo_a <- QC.arbitrary
cFoo_b <- QC.arbitrary
cFoo_c <- QC.arbitrary
pure Acme.MkCFoo{..} test-hs-bindgen/src/Acme/Test/FFI.hs{-# LANGUAGE CApiFFI #-}
module Acme.Test.FFI
( testCFooCToHs
, testCFooHsToC
) where
import qualified Foreign as F
import qualified Foreign.C as FC
import qualified Acme
------------------------------------------------------------------------------
testCFooCToHs :: FC.CChar -> FC.CInt -> FC.CFloat -> IO Acme.CFoo
testCFooCToHs a b c = F.alloca $ \ptr -> do
test_CFoo_CToHs a b c ptr
F.peek ptr
foreign import capi unsafe "test-acme.h test_CFoo_CToHs"
test_CFoo_CToHs ::
FC.CChar
-> FC.CInt
-> FC.CFloat
-> F.Ptr Acme.CFoo
-> IO ()
------------------------------------------------------------------------------
testCFooHsToC :: Acme.CFoo -> FC.CChar -> FC.CInt -> FC.CFloat -> IO Bool
testCFooHsToC x a b c = F.alloca $ \ptr -> do
F.poke ptr x
(/= 0) <$> test_CFoo_HsToC ptr a b c
foreign import capi unsafe "test-acme.h test_CFoo_HsToC"
test_CFoo_HsToC ::
F.Ptr Acme.CFoo
-> FC.CChar
-> FC.CInt
-> FC.CFloat
-> IO FC.CBool test-hs-bindgen/src/Acme/Test.hsmodule Acme.Test (tests) where
import qualified Test.QuickCheck.Monadic as QCM
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (Property, testProperty)
import qualified Acme
import qualified Acme.Test.FFI as FFI
import Acme.Test.Instances ()
------------------------------------------------------------------------------
testCFoo :: TestTree
testCFoo = testGroup "CFoo"
[ testProperty "C->Haskell" prop_CToHs
, testProperty "Haskell->C" prop_HsToC
]
where
prop_CToHs :: Acme.CFoo -> Property
prop_CToHs x = QCM.monadicIO $ do
x' <- QCM.run $
FFI.testCFooCToHs (Acme.cFoo_a x) (Acme.cFoo_b x) (Acme.cFoo_c x)
QCM.assert $ x' == x
prop_HsToC :: Acme.CFoo -> Property
prop_HsToC x = QCM.monadicIO $ do
isSuccess <- QCM.run $
FFI.testCFooHsToC x (Acme.cFoo_a x) (Acme.cFoo_b x) (Acme.cFoo_c x)
QCM.assert isSuccess
------------------------------------------------------------------------------
tests :: TestTree
tests = testGroup "Acme"
[ testCFoo
] I used QuickCheck for this experiment. It has few dependencies, and there are (already) I did not use Allocation and deallocation of memory in tests is all done on the Haskell side, using The C->Haskell test, given an arbitrary value, calls a C function with the values of the fields and a pointer to a structure value. The C function populates the structure value. The QuickCheck property is equality of the value populated in C with the original arbitrary value. The Haskell->C test, given an arbitrary value, calls a C function with a pointer to the structure value and the expected values of the fields. The C function checks that each field is equal to the expected value. I wish that I could output detailed error messages on failure, but QuickCheck does not support that AFAIK. On the C side, we need equality for each field type for any type that we would like to test. This is trivial in this experiment because only primitive types are used, but it complicates things when supporting other types. When we generate tests, perhaps the goal is to generate the whole Thoughts? Is this moving in the right direction? Please feel free to share any corrections and suggestions. |
I added unit tests that check the size and alignment.
The alignment check uses |
I created a hedgehog version of the manual tests. I implemented some generators for the C types used in the test. The generator for The QuickCheck instance does not include special IEEE values at all (which is somewhat disappointing), so Nan is not an issue there. FWIW, I prefer hedgehog over QuickCheck. |
I updated my hedgehog tests to allow NaN, in case we want to do that. On the Haskell side, it uses a On the C side, two library functions are added for checking floats and doubles for representational equality. The generated test code just needs to use those functions where appropriate. We likely should create a We will likely have library functions that are needed by the generated tests. We could create a package for the Haskell side, but the test generator needs to create the C headers and source files anyway, so perhaps it is best to do the same for Haskell modules. The current experiment tests a single "generated" module, but one of our goals is to support generation of multiple modules. If we need to generate instances (such as |
I tried to create instances for the
If there are any suggestions for what types to support in the test library, or which include files I need, I would be happy to hear them. |
@TravisCardwell what this prints on your system:
on my machine it prints 1. |
Thanks @phadej! It prints The behavior is not like Assignment: #include <stdbool.h>
#include <stdio.h>
int main() {
bool b = false;
unsigned char* c = (unsigned char*) &b;
printf("%d %d\n", b, *c);
b = true;
printf("%d %d\n", b, *c);
b = 2;
printf("%d %d\n", b, *c);
return 0;
} Attempting to assign
Addition: #include <stdbool.h>
#include <stdio.h>
int main() {
bool b = false;
printf("%d\n", b);
b += 1;
printf("%d\n", b);
b += 1;
printf("%d\n", b);
b += 2;
printf("%d\n", b);
return 0;
} Addition does not overflow like integral types.
My current (WIP) testing implementation defines instances for One more test#include <stdbool.h>
#include <stdio.h>
typedef struct foo {
bool a;
bool b;
bool c;
} foo;
int main() {
foo x = { .a = true, .b = false, .c = true };
printf("foo: size %lu addr %p\n", sizeof x, &x);
printf("foo.a: size %lu addr %p value %d\n", sizeof x.a, &x.a, x.a);
printf("foo.b: size %lu addr %p value %d\n", sizeof x.b, &x.b, x.b);
printf("foo.c: size %lu addr %p value %d\n", sizeof x.c, &x.c, x.c);
return 0;
}
|
In development of
On a related note, I suggested in chat that we defined all supported types in a |
With the current state of
Notes:
|
FWIW,
Does very little.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct foo {
pub sixty_four: u64,
pub thirty_two: u32,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of foo"][::std::mem::size_of::<foo>() - 16usize];
["Alignment of foo"][::std::mem::align_of::<foo>() - 8usize];
[
"Offset of field: foo::sixty_four",
][::std::mem::offset_of!(foo, sixty_four) - 0usize];
[
"Offset of field: foo::thirty_two",
][::std::mem::offset_of!(foo, thirty_two) - 8usize];
}; Rust doesn't have "Storable", the struct is generated so it's one-to-one with C. We could also generically derive our |
I am currently investigating the idea of generating tests in separate directories per architecture. The goal is to allow users that need to support multiple architectures to easily maintain multiple generated test suites within a single project repository. Cabal conditionals can be used to enable/disable test suites depending on the current architecture. I do not yet understand how we will support multiple architectures, so it is time for some experimentation. I thought that we would need to run When using the preprocessor, we output system-dependent values in the Haskell source code. It therefore seems like we would need to have separate library packages/modules for different architectures. With my current experimental test, I do not think there is anything that is architecture-dependent, as the differences are in the tested library. There are various identifiers that identify the architecture. clang -print-targets
That list is also output by I read that Clang's support for target triplets is just for compatibility with GCC. We use Setting the target to just the architecture (such as Targeting
Targeting Details
Targeting Targeting
Cabal provides the (As I mentioned before, we may want to avoid modifying the user's |
One (untested) idea is to create architecture-specific sub-modules that are included only when building on a matching architecture (using conditions in the
This example illustrates another design decision. Should separate modules for different architectures be created even when they are identical? This example creates separate modules, which I think is a good idea. (BTW, I assume that cross-platform support is only relevant to preprocessor usage, as Template Haskell runs on the target platform.) |
We were using a tuple, but that does not scale well when more data is added. I ran into this when working on annotations (#256, PR #276). I ran into it again when adding source information to support test generation (#22). The `Field` types are used by *both* `Struct`/`Record` and `Newtype` types. (Cherry-picked from `source-info` for experimentation)
When we generate low-level data types with
Storable
instances, we should generate a test suite that verifies that does a "self-test": generate some C code that accepts a pointer to memory, fills it with some values, then on the Haskell side use theStorable
instance to verify that all values are in the right place, and construct an error message if not.Note that such a test failure would be a bug in
hs-bindgen
, so users would not be able to do much with it other than report it (or try to fix it), but at least we'd catch the problem.(Rust
bindgen
generates tests also; it might be useful to take a closer look at exactly they are testing, so see if we can/should do the same.)The text was updated successfully, but these errors were encountered: