feature-test provides foreign feature testing macros and read-syntax. This can be used to alter code at compile-time based on system support for a particular feature.
Feature testing is a time-honored tradition of C programmers who wish to write portable code.
First, the programmer probes the system with a utility such as configure, generating a header file with #defines such as HAVE_IPV6 or HAVE_ADDRINFO. The programmer may also manually generate a header file by examining the system (e.g. with #ifdef) and then defining appropriate HAVE_... macros.
Second, the HAVE_... macros are used to select the appropriate code path at compile-time. If the feature test is simple or rare, the HAVE_... defines are often omitted.
In Chicken, when writing code that interfaces to C, we can naturally use the same technique inside the C code itself. But what if we want to choose a different Scheme code path based on available system features? For example, say we want to choose between defining the following procedures, depending on whether IPv6-only socket binding is available:
(define (ipv6-v6-only? s) (getsockopt s IPPROTO_IPV6 IPV6_V6ONLY)) (define (ipv6-v6-only? s) (error "IPv6 is not supported"))
In that example, IPPROTO_IPV6 and/or IPV6_V6ONLY may not even be #defined on our system, so we cannot simply create a foreign-variable referring to them, as it could result in a compilation error.
Some possible approaches are:
- Write the getsockopt call entirely in C with preprocessor tests that either execute the call or return an "unsupported" value to Scheme, and throw an error based on the result. This can be clumsy and involves a lot of special-case C code.
- Ensure the #define is defined and set to an illegal value, such as -1 if no value may be negative, or 0 in the case of a bitwise flag. Then we can always use a foreign-variable to access the value and select our code path at runtime. Quick and dirty, but sometimes good enough. However, it requires a safe illegal value to exist, and may leave a lot of unused or dead code lying around, which may or may not be optimized out by the C compiler.
- Do the feature test externally (such as in your .setup file) and register a corresponding feature in csc using something like -Dv6only. Then use cond-expand in your code to define the correct procedure at compile time. Can get unwieldy, but does not involve runtime checks or unused C code.
- Use the feature-test egg, which provides a compact and convenient way to do feature testing prior to Scheme compilation.
Feature testing with the feature-test egg proceeds in two phases:
- Determine which foreign features are supported by your system by using C header files and the feature-test extension to register features before compile-time.
- Alter your generated Scheme code by using cond-expand (effective at compile-time), or by using read syntax from the feature-test-syntax extension (effective at read-time).
Determining feature support
To use feature-test, you
- Take any C preprocessor instructions from your main Scheme module and move them into separate include file(s) like myegg.h. Source this file from your module.
- Create a Scheme features file such as myegg-features.scm, sourcing the same C header file(s) as your main module (here myegg.h).
- Use directives from the feature-test module in myegg-features.scm to select which features you'd like visible to your module.
- Compile and execute myegg-features.scm. This will generate a Scheme file on standard output, consisting of one line for each tested feature: either register-feature! if present, or unregister-feature! if absent. Redirect this output to myegg-config.scm.
- Compile your main module, adding -X myegg-config.scm to csc. Features will be registered with the compiler before your code is compiled, so they are visible at read-time and compile-time.
feature-test interface[syntax] (declare-foreign-features FEATURE1 FEATURE2 ...)
For each feature F, tests whether F is #defined in C, and creates a new boolean #define reflecting this. This new #define is prefixed with the declaration-prefix.
For example, using the (default) declaration prefix HAVE_ and the feature AF_UNIX:
(declaration-prefix HAVE_) (declare-foreign-features AF_UNIX)
/* generates the C code */ #ifdef AF_UNIX #define HAVE_AF_UNIX 1 #else #define HAVE_AF_UNIX 0 #endif
The boolean define HAVE_AF_UNIX is now safely visible to a foreign-variable. In contrast, referring to AF_UNIX from Scheme when undefined would result in a compilation error.[syntax] (register-foreign-features FEATURE1 FEATURE2 ...)
For each feature F, accesses the corresponding boolean #define in C, usually generated by declare-foreign-features. Then, generates code to register or unregister the feature for future compiles.
The boolean define is prefixed with the current declaration-prefix, and the registered feature will be prefixed with the registration-prefix.
(declaration-prefix "HAVE_") (registration-prefix "MYEGG_") (register-foreign-features AF_UNIX)
will expand to code like:
(declare-foreign-variable HAVE_AF_UNIX bool "HAVE_AF_UNIX") (if HAVE_AF_UNIX (emit-register! 'MYEGG_AF_UNIX) (emit-unregister! 'MYEGG_AF_UNIX))
And when compiled and executed, the following is printed to standard output
(register-feature! 'MYEGG_AF_UNIX) ;; if AF_UNIX was defined (unregister-feature! 'MYEGG_AF_UNIX) ;; if AF_UNIX was not defined[syntax] (define-foreign-features FEATURE1 FEATURE2 ...)
(declare-foreign-features FEATURE1 FEATURE2 ...) (register-foreign-features FEATURE1 FEATURE2 ...)[syntax] (declaration-prefix X)
Prefix added to the base feature name when declaring a foreign feature. This can be a string or a symbol.
Defaults to HAVE_.[syntax] (registration-prefix X)
Prefix added to the base feature name when registering a foreign feature. This can be a string or a symbol.
Defaults to the empty string.
In this simple and contrived example, we test for the presence of IPPROTO_IPV6 and IPV6_V6ONLY in C, and register or unregister the corresponding features in future compiles. On Windows 2000 and XP, for example, IPPROTO_IPV6 is defined but IPV6_V6ONLY is not.
Our test module mysock.scm allows you to create an IPv6 socket and test its IPv6 bind-only option status; if the option is unavailable, this is detected at compile time and the test is defined to throw a Scheme error. Our little socket6 procedure makes some assumptions of its own, but it's only for illustration.
A real example can be found in the source code to the socket egg.
/* mysock.h */ #ifdef _WIN32 # include <winsock2.h> # include <ws2tcpip.h> #else # include <netinet/in.h> # include <sys/socket.h> #endif
;;; mysock-features.scm #> #include "mysock.h" <# (use feature-test) (declaration-prefix HAVE_) ;; Exact value not important here. (registration-prefix "") (define-foreign-features IPPROTO_IPV6 IPV6_V6ONLY)
;;; mysock.setup (compile mysock-features.scm) (run (./mysock-features > mysock-config.scm)) (compile -sJ -X mysock-config.scm mysock.scm) ;; [install-extension is omitted for our test]
;;; mysock.meta ;; Can be left blank for our example ;; but needs to exist even for chicken-install -n.
;;; mysock-config.scm (generated on linux) (register-feature! 'IPPROTO_IPV6) (register-feature! 'IPV6_V6ONLY)
;;; mysock-config.scm (generated on mingw32, pre-Vista) (register-feature! 'IPPROTO_IPV6) (unregister-feature! 'IPV6_V6ONLY)
;;; mysock.scm #> #include "mysock.h" <# (module mysock (ipv6-v6-only? socket6) (import scheme chicken foreign) ;; Get integer socket option NAME on fd SOCK at LEVEL (define getsockopt (foreign-lambda* int ((int sock) (int level) (int name)) "int ret; socklen_t sz = sizeof(ret);" "if (getsockopt(sock, level, name, (void *)&ret, &sz) < 0)" " C_return(-1);" "C_return(ret);")) ;; Create an IPv6 TCP socket and return its file descriptor ;; for testing purposes. Assume AF_INET6 and SOCK_STREAM are defined. (define socket6 (foreign-lambda* int () "C_return(socket(AF_INET6,SOCK_STREAM,0));")) (cond-expand ((and IPPROTO_IPV6 IPV6_V6ONLY) (define-foreign-variable _ipproto_ipv6 int "IPPROTO_IPV6") (define-foreign-variable _ipv6_v6only int "IPV6_V6ONLY") (define (ipv6-v6-only? s) (getsockopt s _ipproto_ipv6 _ipv6_v6only))) (else (define (ipv6-v6-only? s) (error "IPv6 only binding is not supported")))))
### Build $ chicken-install -n mysock.setup ### Test on UNIX $ csi -R mysock -p "(ipv6-v6-only? (socket6))" 0 ### Test on Windows XP > csi -R mysock -p "(ipv6-v6-only? (socket6))" Error: IPv6 only binding is not supported
Acting on feature support
cond-expand is the usual way to test and act on feature support. However, cond-expand works at macroexpansion time, as does the default #+ reader macro. Therefore, it generally cannot be used inside macros.
To address this, reader macros that do feature testing at read-time are provided in the feature-test-syntax extension. Use it like:
csc -X feature-test-syntax myegg.scm[read] #+
Test FEATURE at read-time and, if present, expand to EXPR. FEATURE may be any feature expression permitted in a cond-expand, such as windows or (and windows macosx).
#+ can be used inside macros because it is expanded when the macro form is read, prior to macroexpansion. However, this requires a Chicken version >= 4.6.7, which will omit EXPR if the feature test is false. In earlier versions, the test expands to a (void) form, like the built-in #+. (void) forms are usually illegal inside macro bodies.
Here is an example that assumes Chicken is at least 4.6.7, which will omit EXPR on a false test:
(cond ((eq? x _af_inet) "internet address family") #+AF_UNIX ((eq? x _af_unix) "unix address family") (else "unknown address family")
If AF_UNIX is a registered feature at compile-time, it will be read as:
(cond ((eq? x _af_inet) "internet address family") ((eq? x _af_unix) "unix address family") (else "unknown address family")
If AF_UNIX is not a registered feature, it will be read as:
(cond ((eq? x _af_inet) "internet address family") (else "unknown address family")
However, if AF_UNIX is unregistered and you are using Chicken prior to 4.6.7, it will instead expand into the illegal:
(cond ((eq? x _af_inet) "internet address family") (##core#undefined) ((eq? x _af_unix) "unix address family") (else "unknown address family")
So be careful.[read] #-
Like #+, but of opposite polarity.[read] #?
#?(FEATURE CONSEQUENT ALTERNATE)
Perform an if-then test at read-time on FEATURE, expanding to CONSEQUENT if FEATURE is present or ALTERNATE if absent. FEATURE may be any feature expression permitted in a cond-expand, such as windows or (and windows macosx).
#? can be used inside macros because it is expanded when the macro form is read, prior to macroexpansion. It expands correctly irrespective of Chicken version.
#? is similar to the Common Lisp idiom
#+FEATURE CONSEQUENT #-FEATURE ALTERNATE
and, in Chicken versions >= 4.6.7 it is exactly equivalent, even inside macro bodies. However, in previous versions #+ and #- will not work properly inside macros; see #+ for further explanation.
An example of #? which is essentially equivalent to cond-expand:
(define af/unix #?(AF_UNIX _af_unix #f)) ;; is basically the same as (define af/unix (cond-expand (AF_UNIX _af_unix) (else #f)))
A more powerful example of #?:
(cond ((eq? x _af_inet) "internet address family") #?(AF_UNIX ((eq? x _af_unix) "unix address family") (#f)) (else "unknown address family")
which, if AF_UNIX is a registered feature, expands into
(cond ((eq? x _af_inet) "internet address family") ((eq? x _af_unix) "unix address family") (else "unknown address family")
and if not, expands into
(cond ((eq? x _af_inet) "internet address family") (#f) (else "unknown address family")
In the latter case, the false clause cannot succeed and is hopefully optimized out by the compiler. #+ would be more appropriate, but requires Chicken >= 4.6.7. This technique doesn't work with every macro, but you do what you can.
Bugs and limitations
- As if it hasn't been repeated enough times, you need Chicken 4.6.7 to safely use #+ and #- read syntax within macro bodies. Barring that, stick to #?.
About this egg
- Initial release
Copyright (c) 2011 Jim Ursetto. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.