217 changes: 217 additions & 0 deletions clang-tools-extra/clangd/xpc/XPCTransport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//===--- XPCTransport.cpp - sending and receiving LSP messages over XPC ---===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
#include "Conversion.h"
#include "Logger.h"
#include "Protocol.h" // For LSPError
#include "Transport.h"
#include "llvm/Support/Errno.h"

#include <xpc/xpc.h>

using namespace llvm;
using namespace clang;
using namespace clangd;

namespace {

json::Object encodeError(Error E) {
std::string Message;
ErrorCode Code = ErrorCode::UnknownErrorCode;
if (Error Unhandled =
handleErrors(std::move(E), [&](const LSPError &L) -> Error {
Message = L.Message;
Code = L.Code;
return Error::success();
}))
Message = toString(std::move(Unhandled));

return json::Object{
{"message", std::move(Message)},
{"code", int64_t(Code)},
};
}

Error decodeError(const json::Object &O) {
std::string Msg = O.getString("message").getValueOr("Unspecified error");
if (auto Code = O.getInteger("code"))
return make_error<LSPError>(std::move(Msg), ErrorCode(*Code));
return make_error<StringError>(std::move(Msg), inconvertibleErrorCode());
}

// C "closure" for XPCTransport::loop() method
namespace xpcClosure {
void connection_handler(xpc_connection_t clientConnection);
}

class XPCTransport : public Transport {
public:
XPCTransport() {}

void notify(StringRef Method, json::Value Params) override {
sendMessage(json::Object{
{"jsonrpc", "2.0"},
{"method", Method},
{"params", std::move(Params)},
});
}
void call(StringRef Method, json::Value Params, json::Value ID) override {
sendMessage(json::Object{
{"jsonrpc", "2.0"},
{"id", std::move(ID)},
{"method", Method},
{"params", std::move(Params)},
});
}
void reply(json::Value ID, Expected<json::Value> Result) override {
if (Result) {
sendMessage(json::Object{
{"jsonrpc", "2.0"},
{"id", std::move(ID)},
{"result", std::move(*Result)},
});
} else {
sendMessage(json::Object{
{"jsonrpc", "2.0"},
{"id", std::move(ID)},
{"error", encodeError(Result.takeError())},
});
}
}

Error loop(MessageHandler &Handler) override;

private:
// Needs access to handleMessage() and resetClientConnection()
friend void xpcClosure::connection_handler(xpc_connection_t clientConnection);

// Dispatches incoming message to Handler onNotify/onCall/onReply.
bool handleMessage(json::Value Message, MessageHandler &Handler);
void sendMessage(json::Value Message) {
xpc_object_t response = jsonToXpc(Message);
xpc_connection_send_message(clientConnection, response);
xpc_release(response);
}
void resetClientConnection(xpc_connection_t newClientConnection) {
clientConnection = newClientConnection;
}
xpc_connection_t clientConnection;
};

bool XPCTransport::handleMessage(json::Value Message, MessageHandler &Handler) {
// Message must be an object with "jsonrpc":"2.0".
auto *Object = Message.getAsObject();
if (!Object || Object->getString("jsonrpc") != Optional<StringRef>("2.0")) {
elog("Not a JSON-RPC 2.0 message: {0:2}", Message);
return false;
}
// ID may be any JSON value. If absent, this is a notification.
Optional<json::Value> ID;
if (auto *I = Object->get("id"))
ID = std::move(*I);
auto Method = Object->getString("method");
if (!Method) { // This is a response.
if (!ID) {
elog("No method and no response ID: {0:2}", Message);
return false;
}
if (auto *Err = Object->getObject("error"))
return Handler.onReply(std::move(*ID), decodeError(*Err));
// Result should be given, use null if not.
json::Value Result = nullptr;
if (auto *R = Object->get("result"))
Result = std::move(*R);
return Handler.onReply(std::move(*ID), std::move(Result));
}
// Params should be given, use null if not.
json::Value Params = nullptr;
if (auto *P = Object->get("params"))
Params = std::move(*P);

if (ID)
return Handler.onCall(*Method, std::move(Params), std::move(*ID));
else
return Handler.onNotify(*Method, std::move(Params));
}

namespace xpcClosure {
// "owner" of this "closure object" - necessary for propagating connection to
// XPCTransport so it can send messages to the client.
XPCTransport *TransportObject = nullptr;
Transport::MessageHandler *HandlerPtr = nullptr;

void connection_handler(xpc_connection_t clientConnection) {
xpc_connection_set_target_queue(clientConnection, dispatch_get_main_queue());

xpc_transaction_begin();

TransportObject->resetClientConnection(clientConnection);

xpc_connection_set_event_handler(clientConnection, ^(xpc_object_t message) {
if (message == XPC_ERROR_CONNECTION_INVALID) {
// connection is being terminated
log("Received XPC_ERROR_CONNECTION_INVALID message - returning from the "
"event_handler.");
return;
}

if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) {
log("Received XPC message of unknown type - returning from the "
"event_handler.");
return;
}

const json::Value Doc = xpcToJson(message);
if (Doc == json::Value(nullptr)) {
log("XPC message was converted to Null JSON message - returning from the "
"event_handler.");
return;
}

vlog("<<< {0}\n", Doc);

if (!TransportObject->handleMessage(std::move(Doc), *HandlerPtr)) {
log("Received exit notification - cancelling connection.");
xpc_connection_cancel(xpc_dictionary_get_remote_connection(message));
xpc_transaction_end();
}
});

xpc_connection_resume(clientConnection);
}
} // namespace xpcClosure

Error XPCTransport::loop(MessageHandler &Handler) {
assert(xpcClosure::TransportObject == nullptr &&
"TransportObject has already been set.");
// This looks scary since lifetime of this (or any) XPCTransport object has
// to fully contain lifetime of any XPC connection. In practise any Transport
// object is destroyed only at the end of main() which is always after
// exit of xpc_main().
xpcClosure::TransportObject = this;

assert(xpcClosure::HandlerPtr == nullptr &&
"HandlerPtr has already been set.");
xpcClosure::HandlerPtr = &Handler;

xpc_main(xpcClosure::connection_handler);
// xpc_main doesn't ever return
return errorCodeToError(std::make_error_code(std::errc::io_error));
}

} // namespace

namespace clang {
namespace clangd {

std::unique_ptr<Transport> newXPCTransport() {
return llvm::make_unique<XPCTransport>();
}

} // namespace clangd
} // namespace clang
28 changes: 28 additions & 0 deletions clang-tools-extra/clangd/xpc/cmake/Info.plist.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${CLANGD_XPC_FRAMEWORK_NAME}</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>org.llvm.${CLANGD_XPC_FRAMEWORK_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${CLANGD_XPC_FRAMEWORK_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string></string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CSResourcesFileMapped</key>
<true/>
</dict>
</plist>
30 changes: 30 additions & 0 deletions clang-tools-extra/clangd/xpc/cmake/XPCServiceInfo.plist.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>${CLANGD_XPC_SERVICE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${CLANGD_XPC_SERVICE_BUNDLE_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${CLANGD_XPC_SERVICE_NAME}</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleVersion</key>
<string></string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
<key>EnvironmentVariables</key>
<dict>
<key>CLANGD_AS_XPC_SERVICE</key>
<string>1</string>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Creates the ClangdXPC framework.
macro(create_clangd_xpc_framework target name)
set(CLANGD_FRAMEWORK_LOCATION "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${name}.framework")
set(CLANGD_FRAMEWORK_OUT_LOCATION "${CLANGD_FRAMEWORK_LOCATION}/Versions/A")

# Create the framework info PLIST.
set(CLANGD_XPC_FRAMEWORK_NAME "${name}")
configure_file(
"${CLANGD_XPC_SOURCE_DIR}/cmake/Info.plist.in"
"${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist")

set(CLANGD_XPC_SERVICE_NAME "clangd")
set(CLANGD_XPC_SERVICE_OUT_LOCATION
"${CLANGD_FRAMEWORK_OUT_LOCATION}/XPCServices/${CLANGD_XPC_SERVICE_NAME}.xpc/Contents")

# Create the XPC service info PLIST.
set(CLANGD_XPC_SERVICE_BUNDLE_NAME "org.llvm.${CLANGD_XPC_SERVICE_NAME}")
configure_file(
"${CLANGD_XPC_SOURCE_DIR}/cmake/XPCServiceInfo.plist.in"
"${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist")

# Create the custom command
add_custom_command(OUTPUT ${CLANGD_FRAMEWORK_LOCATION}
# Copy the PLIST.
COMMAND ${CMAKE_COMMAND} -E copy
"${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist"
"${CLANGD_FRAMEWORK_OUT_LOCATION}/Resources/Info.plist"

# Copy the framework binary.
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${target}.dylib"
"${CLANGD_FRAMEWORK_OUT_LOCATION}/${name}"

# Copy the XPC Service PLIST.
COMMAND ${CMAKE_COMMAND} -E copy
"${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist"
"${CLANGD_XPC_SERVICE_OUT_LOCATION}/Info.plist"

# Copy the Clangd binary.
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/clangd"
"${CLANGD_XPC_SERVICE_OUT_LOCATION}/MacOS/clangd"

COMMAND ${CMAKE_COMMAND} -E create_symlink "A"
"${CLANGD_FRAMEWORK_LOCATION}/Versions/Current"

COMMAND ${CMAKE_COMMAND} -E create_symlink
"Versions/Current/Resources"
"${CLANGD_FRAMEWORK_LOCATION}/Resources"

COMMAND ${CMAKE_COMMAND} -E create_symlink
"Versions/Current/XPCServices"
"${CLANGD_FRAMEWORK_LOCATION}/XPCServices"

COMMAND ${CMAKE_COMMAND} -E create_symlink
"Versions/Current/${name}"
"${CLANGD_FRAMEWORK_LOCATION}/${name}"

DEPENDS
"${CLANGD_XPC_BINARY_DIR}/${name}.Info.plist"
"${CLANGD_XPC_BINARY_DIR}/${name}Service.Info.plist"
clangd
COMMENT "Creating ClangdXPC framework"
VERBATIM
)

add_custom_target(
ClangdXPC
DEPENDS
${target}
${CLANGD_FRAMEWORK_LOCATION}
)
endmacro(create_clangd_xpc_framework)
9 changes: 9 additions & 0 deletions clang-tools-extra/clangd/xpc/framework/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

set(SOURCES
ClangdXPC.cpp)
add_clang_library(ClangdXPCLib SHARED
${SOURCES}
DEPENDS
clangd
)
create_clangd_xpc_framework(ClangdXPCLib "ClangdXPC")
5 changes: 5 additions & 0 deletions clang-tools-extra/clangd/xpc/framework/ClangdXPC.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

/// Returns the bundle identifier of the Clangd XPC service.
extern "C" const char *clangd_xpc_get_bundle_identifier() {
return "org.llvm.clangd";
}
26 changes: 26 additions & 0 deletions clang-tools-extra/clangd/xpc/test-client/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/../../
)

add_clang_tool(
clangd-xpc-test-client
ClangdXPCTestClient.cpp

DEPENDS ClangdXPC
)

set(LLVM_LINK_COMPONENTS
support
)

target_link_libraries(clangd-xpc-test-client
PRIVATE
clangBasic
clangDaemon
clangFormat
clangFrontend
clangSema
clangTooling
clangToolingCore
clangdXpcJsonConversions
)
96 changes: 96 additions & 0 deletions clang-tools-extra/clangd/xpc/test-client/ClangdXPCTestClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include "xpc/Conversion.h"
#include "clang/Basic/LLVM.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/LineIterator.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <dlfcn.h>
#include <stdio.h>
#include <string>
#include <xpc/xpc.h>

typedef const char *(*clangd_xpc_get_bundle_identifier_t)(void);

using namespace llvm;
using namespace clang;

std::string getLibraryPath() {
Dl_info info;
if (dladdr((void *)(uintptr_t)getLibraryPath, &info) == 0)
llvm_unreachable("Call to dladdr() failed");
llvm::SmallString<128> LibClangPath;
LibClangPath = llvm::sys::path::parent_path(
llvm::sys::path::parent_path(info.dli_fname));
llvm::sys::path::append(LibClangPath, "lib", "ClangdXPC.framework",
"ClangdXPC");
return LibClangPath.str();
}

static void dumpXPCObject(xpc_object_t Object, llvm::raw_ostream &OS) {
xpc_type_t Type = xpc_get_type(Object);
if (Type == XPC_TYPE_DICTIONARY) {
json::Value Json = clang::clangd::xpcToJson(Object);
OS << Json;
} else {
OS << "<UNKNOWN>";
}
}

int main(int argc, char *argv[]) {
// Open the ClangdXPC dylib in the framework.
std::string LibPath = getLibraryPath();
void *dlHandle = dlopen(LibPath.c_str(), RTLD_LOCAL | RTLD_FIRST);
if (!dlHandle)
return 1;

// Lookup the XPC service bundle name, and launch it.
clangd_xpc_get_bundle_identifier_t clangd_xpc_get_bundle_identifier =
(clangd_xpc_get_bundle_identifier_t)dlsym(
dlHandle, "clangd_xpc_get_bundle_identifier");
xpc_connection_t conn = xpc_connection_create(
clangd_xpc_get_bundle_identifier(), dispatch_get_main_queue());

// Dump the XPC events.
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
if (event == XPC_ERROR_CONNECTION_INVALID) {
llvm::errs() << "Received XPC_ERROR_CONNECTION_INVALID.";
exit(EXIT_SUCCESS);
}
if (event == XPC_ERROR_CONNECTION_INTERRUPTED) {
llvm::errs() << "Received XPC_ERROR_CONNECTION_INTERRUPTED.";
exit(EXIT_SUCCESS);
}

dumpXPCObject(event, llvm::outs());
llvm::outs() << "\n";
});

xpc_connection_resume(conn);

// Read the input to determine the things to send to clangd.
llvm::ErrorOr<std::unique_ptr<MemoryBuffer>> Stdin =
llvm::MemoryBuffer::getSTDIN();
if (!Stdin) {
llvm::errs() << "Failed to get STDIN!\n";
return 1;
}
for (llvm::line_iterator It(**Stdin, /*SkipBlanks=*/true,
/*CommentMarker=*/'#');
!It.is_at_eof(); ++It) {
StringRef Line = *It;
if (auto Request = json::parse(Line)) {
xpc_object_t Object = clangd::jsonToXpc(*Request);
xpc_connection_send_message(conn, Object);
} else {
llvm::errs() << llvm::Twine("JSON parse error: ")
<< llvm::toString(Request.takeError());
return 1;
}
}

dispatch_main();

// dispatch_main() doesn't return
return EXIT_FAILURE;
}
9 changes: 7 additions & 2 deletions clang-tools-extra/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ endif ()
string(REPLACE ${CMAKE_CFG_INTDIR} ${LLVM_BUILD_MODE} CLANG_TOOLS_DIR ${LLVM_RUNTIME_OUTPUT_INTDIR})

llvm_canonicalize_cmake_booleans(
CLANG_ENABLE_STATIC_ANALYZER)
CLANG_ENABLE_STATIC_ANALYZER
CLANGD_BUILD_XPC_SUPPORT)

configure_lit_site_cfg(
${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in
Expand Down Expand Up @@ -61,7 +62,11 @@ set(CLANG_TOOLS_TEST_DEPS
clang-headers

clang-tidy
)
)

if(CLANGD_BUILD_XPC_SUPPORT)
list(APPEND CLANG_TOOLS_TEST_DEPS clangd-xpc-test-client)
endif()

set(CLANGD_TEST_DEPS
clangd
Expand Down
10 changes: 10 additions & 0 deletions clang-tools-extra/test/clangd/xpc/initialize.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# RUN: clangd-xpc-test-client < %s | FileCheck %s
# REQUIRES: clangd-xpc-support

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}}
# CHECK: {"id":0,"jsonrpc":"2.0","result":{"capabilities"

{"jsonrpc":"2.0","id":3,"method":"shutdown"}
# CHECK: {"id":3,"jsonrpc":"2.0","result":null}

{"jsonrpc":"2.0","method":"exit"}
4 changes: 4 additions & 0 deletions clang-tools-extra/test/lit.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ if not platform.system() in ['Windows'] or not execute_external:
if platform.system() not in ['Windows']:
config.available_features.add('ansi-escape-sequences')

# XPC support for Clangd.
if config.clangd_xpc_support:
config.available_features.add('clangd-xpc-support')

if config.clang_staticanalyzer:
config.available_features.add('static-analyzer')

Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/test/lit.site.cfg.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ config.clang_libs_dir = "@SHLIBDIR@"
config.python_executable = "@PYTHON_EXECUTABLE@"
config.target_triple = "@TARGET_TRIPLE@"
config.clang_staticanalyzer = @CLANG_ENABLE_STATIC_ANALYZER@
config.clangd_xpc_support = @CLANGD_BUILD_XPC_SUPPORT@

# Support substitution of the tools and libs dirs with user parameters. This is
# used when we can't determine the tool dir at configuration time.
Expand Down
4 changes: 4 additions & 0 deletions clang-tools-extra/unittests/clangd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ target_link_libraries(ClangdTests
LLVMSupport
LLVMTestingSupport
)

if (CLANGD_BUILD_XPC)
add_subdirectory(xpc)
endif ()
21 changes: 21 additions & 0 deletions clang-tools-extra/unittests/clangd/xpc/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
set(LLVM_LINK_COMPONENTS
support
)

get_filename_component(CLANGD_SOURCE_DIR
${CMAKE_CURRENT_SOURCE_DIR}/../../clangd REALPATH)
include_directories(
${CLANGD_SOURCE_DIR}
)

add_extra_unittest(ClangdXpcTests
ConversionTests.cpp
)

target_link_libraries(ClangdXpcTests
PRIVATE
clangdXpcJsonConversions
clangDaemon
LLVMSupport
LLVMTestingSupport
)
36 changes: 36 additions & 0 deletions clang-tools-extra/unittests/clangd/xpc/ConversionTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//===-- ConversionTests.cpp --------------------------*- C++ -*-----------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//

#include "xpc/Conversion.h"
#include "gtest/gtest.h"

#include <limits>

namespace clang {
namespace clangd {
namespace {

using namespace llvm;

TEST(JsonXpcConversionTest, JsonToXpcToJson) {

for (auto &testcase :
{json::Value(false), json::Value(3.14), json::Value(42),
json::Value(-100), json::Value("foo"), json::Value(""),
json::Value("123"), json::Value(" "),
json::Value{true, "foo", nullptr, 42},
json::Value(json::Object{
{"a", true}, {"b", "foo"}, {"c", nullptr}, {"d", 42}})}) {
EXPECT_TRUE(testcase == xpcToJson(jsonToXpc(testcase))) << testcase;
}
}

} // namespace
} // namespace clangd
} // namespace clang