From 7f79efeccbed356eff6f6ce5e6483f2bb042e97b Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Sun, 11 Oct 2026 01:26:39 +0200 Subject: [PATCH] Fix Scala Native curl backend on ARM (Macs) Backport of #2082 to v3: avoid calling variadic curl_easy_setopt/getinfo directly via FFI, which breaks on arm64 macOS (URL_MALFORMAT). Use non-variadic C wrappers instead. POST with empty body now sets empty POSTFIELDS to avoid waiting on stdin. --- core/src/main/resources/scala-native/ffi.c | 9 ++++ .../sttp/client3/AbstractCurlBackend.scala | 10 +++-- .../scalanative/sttp/client3/curl/CCurl.scala | 22 ++++++---- .../sttp/client3/curl/CurlApi.scala | 42 +++++++------------ 4 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 core/src/main/resources/scala-native/ffi.c diff --git a/core/src/main/resources/scala-native/ffi.c b/core/src/main/resources/scala-native/ffi.c new file mode 100644 index 0000000000..3595bb6efb --- /dev/null +++ b/core/src/main/resources/scala-native/ffi.c @@ -0,0 +1,9 @@ +#if defined(STTP_CURL_FFI) +#include + +int sttp_curl_setopt_int(CURL *curl, CURLoption opt, int arg) {return curl_easy_setopt(curl, opt, arg); } +int sttp_curl_setopt_long(CURL *curl, CURLoption opt, long arg) {return curl_easy_setopt(curl, opt, arg); } +int sttp_curl_setopt_pointer(CURL *curl, CURLoption opt, void* arg) {return curl_easy_setopt(curl, opt, arg); } + +int sttp_curl_getinfo_pointer(CURL *curl, CURLINFO info, void* arg) {return curl_easy_getinfo(curl, info, arg); } +#endif diff --git a/core/src/main/scalanative/sttp/client3/AbstractCurlBackend.scala b/core/src/main/scalanative/sttp/client3/AbstractCurlBackend.scala index 692cdd986d..3e655c41b4 100644 --- a/core/src/main/scalanative/sttp/client3/AbstractCurlBackend.scala +++ b/core/src/main/scalanative/sttp/client3/AbstractCurlBackend.scala @@ -111,7 +111,7 @@ abstract class AbstractCurlBackend[F[_]](monad: MonadError[F], verbose: Boolean) curl.option(HeaderData, spaces.headersResp) curl.option(Url, request.uri.toString) setMethod(curl, request.method) - setRequestBody(curl, request.body) + setRequestBody(curl, request.body, request.method) responseMonad.flatMap(lift(curl.perform)) { _ => curl.info(ResponseCode, spaces.httpCode) val responseBody = fromCString((!spaces.bodyResp)._1) @@ -152,7 +152,7 @@ abstract class AbstractCurlBackend[F[_]](monad: MonadError[F], verbose: Boolean) curl.option(WriteData, outputFilePtr) curl.option(Url, request.uri.toString) setMethod(curl, request.method) - setRequestBody(curl, request.body) + setRequestBody(curl, request.body, request.method) responseMonad.flatMap(lift(curl.perform)) { _ => curl.info(ResponseCode, spaces.httpCode) val httpCode = StatusCode((!spaces.httpCode).toInt) @@ -192,7 +192,7 @@ abstract class AbstractCurlBackend[F[_]](monad: MonadError[F], verbose: Boolean) lift(m) } - private def setRequestBody[R >: PE](curl: CurlHandle, body: RequestBody[R])(implicit zone: Zone): F[CurlCode] = + private def setRequestBody[R >: PE](curl: CurlHandle, body: RequestBody[R], method: Method)(implicit zone: Zone): F[CurlCode] = body match { // todo: assign to responseMonad object case b: BasicRequestBody => val str = basicBodyToString(b) @@ -218,7 +218,9 @@ abstract class AbstractCurlBackend[F[_]](monad: MonadError[F], verbose: Boolean) case StreamBody(_) => responseMonad.error(new IllegalStateException("CurlBackend does not support stream request body")) case NoBody => - responseMonad.unit(CurlCode.Ok) + // POST with empty body might wait for the input on stdin + if (method.is(Method.POST)) lift(curl.option(PostFields, c"")) + else responseMonad.unit(CurlCode.Ok) } private def basicBodyToString(body: RequestBody[_]): String = diff --git a/core/src/main/scalanative/sttp/client3/curl/CCurl.scala b/core/src/main/scalanative/sttp/client3/curl/CCurl.scala index 1af62278f2..1ea2dee89c 100644 --- a/core/src/main/scalanative/sttp/client3/curl/CCurl.scala +++ b/core/src/main/scalanative/sttp/client3/curl/CCurl.scala @@ -12,25 +12,29 @@ private[curl] trait MimePart {} @link("curl") @extern +@define("STTP_CURL_FFI") private[curl] object CCurl { + @name("sttp_curl_setopt_int") + def setoptInt(handle: Ptr[Curl], option: CInt, parameter: Int): CInt = extern + + @name("sttp_curl_setopt_long") + def setoptLong(handle: Ptr[Curl], option: CInt, parameter: Long): CInt = extern + + @name("sttp_curl_setopt_pointer") + def setoptPtr(handle: Ptr[Curl], option: CInt, parameter: Ptr[_]): CInt = extern + + @name("sttp_curl_getinfo_pointer") + def getInfo(handle: Ptr[Curl], info: CInt, parameter: Ptr[_]): CInt = extern + @name("curl_easy_init") def init: Ptr[Curl] = extern @name("curl_easy_cleanup") def cleanup(handle: Ptr[Curl]): Unit = extern - @name("curl_easy_setopt") - def setopt(handle: Ptr[Curl], option: CInt, parameter: Ptr[_]): CInt = extern - - @name("curl_easy_setopt") - def setopt(handle: Ptr[Curl], option: CInt, parameter: CVarArgList): CInt = extern - @name("curl_easy_perform") def perform(easy_handle: Ptr[Curl]): CInt = extern - @name("curl_easy_getinfo") - def getInfo(handle: Ptr[Curl], info: CInt, parameter: Ptr[_]): CInt = extern - @name("curl_mime_init") def mimeInit(easy: Ptr[Curl]): Ptr[Mime] = extern diff --git a/core/src/main/scalanative/sttp/client3/curl/CurlApi.scala b/core/src/main/scalanative/sttp/client3/curl/CurlApi.scala index 42a4456de8..619cd8a20e 100644 --- a/core/src/main/scalanative/sttp/client3/curl/CurlApi.scala +++ b/core/src/main/scalanative/sttp/client3/curl/CurlApi.scala @@ -4,9 +4,9 @@ import sttp.client3.curl.CurlCode.CurlCode import sttp.client3.curl.CurlInfo.CurlInfo import sttp.client3.curl.CurlOption.CurlOption -import scala.scalanative.runtime.Boxes import scala.scalanative.unsafe.{Ptr, _} import scala.scalanative.unsigned._ +import java.nio.charset.StandardCharsets private[client3] object CurlApi { type CurlHandle = Ptr[Curl] @@ -26,29 +26,23 @@ private[client3] object CurlApi { def cleanup(): Unit = CCurl.cleanup(handle) - def option(option: CurlOption, parameter: String)(implicit z: Zone): CurlCode = { - setopt(handle, option, toCString(parameter)) - } + def option(option: CurlOption, parameter: String)(implicit z: Zone): CurlCode = + this.option(option, toCString(parameter, StandardCharsets.UTF_8)) - def option(option: CurlOption, parameter: Long)(implicit z: Zone): CurlCode = { - setopt(handle, option, parameter) - } + def option(option: CurlOption, parameter: Long): CurlCode = + CurlCode(CCurl.setoptLong(handle, option.id, parameter)) - def option(option: CurlOption, parameter: Int)(implicit z: Zone): CurlCode = { - setopt(handle, option, parameter) - } + def option(option: CurlOption, parameter: Int): CurlCode = + CurlCode(CCurl.setoptInt(handle, option.id, parameter)) - def option(option: CurlOption, parameter: Boolean)(implicit z: Zone): CurlCode = { - setopt(handle, option, if (parameter) 1L else 0L) - } + def option(option: CurlOption, parameter: Boolean): CurlCode = + this.option(option, if (parameter) 1 else 0) - def option(option: CurlOption, parameter: Ptr[_]): CurlCode = { - setopt(handle, option, parameter) - } + def option(option: CurlOption, parameter: Ptr[_]): CurlCode = + CurlCode(CCurl.setoptPtr(handle, option.id, parameter)) - def option[FuncPtr <: CFuncPtr](option: CurlOption, parameter: FuncPtr)(implicit z: Zone): CurlCode = { - setopt(handle, option, Boxes.boxToPtr[Byte](Boxes.unboxToCFuncPtr0(parameter))) - } + def option(option: CurlOption, parameter: CFuncPtr): CurlCode = + this.option(option, CFuncPtr.toPtr(parameter)) def info(curlInfo: CurlInfo, parameter: Long)(implicit z: Zone): CurlCode = { val lPtr = alloc[Long](sizeof[Long]) @@ -57,7 +51,7 @@ private[client3] object CurlApi { } def info(curlInfo: CurlInfo, parameter: String)(implicit z: Zone): CurlCode = { - getInfo(handle, curlInfo, toCString(parameter)) + getInfo(handle, curlInfo, toCString(parameter, StandardCharsets.UTF_8)) } def info(curlInfo: CurlInfo, parameter: Ptr[_]): CurlCode = { @@ -65,14 +59,6 @@ private[client3] object CurlApi { } } - private def setopt(handle: CurlHandle, option: CurlOption, parameter: Ptr[_]): CurlCode = { - CurlCode(CCurl.setopt(handle, option.id, parameter)) - } - - private def setopt(handle: CurlHandle, option: CurlOption, parameter: CVarArg)(implicit z: Zone): CurlCode = { - CurlCode(CCurl.setopt(handle, option.id, toCVarArgList(Seq(parameter)))) - } - private def getInfo(handle: CurlHandle, curlInfo: CurlInfo, parameter: Ptr[_]): CurlCode = { CurlCode(CCurl.getInfo(handle, curlInfo.id, parameter)) }